diff --git a/implementations/pko/01_pko_data_exploration.ipynb b/implementations/pko/01_pko_data_exploration.ipynb
new file mode 100644
index 00000000..0ae932a7
--- /dev/null
+++ b/implementations/pko/01_pko_data_exploration.ipynb
@@ -0,0 +1,7246 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "d4809da6",
+ "metadata": {},
+ "source": [
+ "# Palm Oil — Data Exploration\n",
+ "\n",
+ "A visual tour of the palm oil price series behind the PKO experiment, aimed at two\n",
+ "decisions: **which series to forecast**, and **which 7 cutoffs to forecast from**.\n",
+ "\n",
+ "The target is FRED `PPOILUSDM` — the IMF global benchmark palm oil price, monthly,\n",
+ "USD per metric ton, registered with true publication dates by `pko.data`.\n",
+ "\n",
+ "See [`DATA.md`](DATA.md) for the full survey of what FRED carries and why this series\n",
+ "was chosen. Every chart below is interactive: **click a legend entry to hide a series**,\n",
+ "drag to zoom, double-click to reset.\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "e3e658a7",
+ "metadata": {},
+ "source": [
+ "---\n",
+ "## 1. Load the price series\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "91f708b7",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-08-06T19:18:01.529992Z",
+ "iopub.status.busy": "2026-08-06T19:18:01.529679Z",
+ "iopub.status.idle": "2026-08-06T19:18:02.193121Z",
+ "shell.execute_reply": "2026-08-06T19:18:02.191177Z"
+ }
+ },
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "414 monthly observations\n",
+ "span : 1992-01 -> 2026-06\n",
+ "price : $185 to $1653 per tonne\n"
+ ]
+ },
+ {
+ "data": {
+ "text/html": [
+ "
\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " | \n",
+ " timestamp | \n",
+ " value | \n",
+ " released_at | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " | 409 | \n",
+ " 2026-02-01 | \n",
+ " 1033.768394 | \n",
+ " 2026-03-24 | \n",
+ "
\n",
+ " \n",
+ " | 410 | \n",
+ " 2026-03-01 | \n",
+ " 1121.177897 | \n",
+ " 2026-04-15 | \n",
+ "
\n",
+ " \n",
+ " | 411 | \n",
+ " 2026-04-01 | \n",
+ " 1137.410862 | \n",
+ " 2026-06-05 | \n",
+ "
\n",
+ " \n",
+ " | 412 | \n",
+ " 2026-05-01 | \n",
+ " 1130.026757 | \n",
+ " 2026-06-05 | \n",
+ "
\n",
+ " \n",
+ " | 413 | \n",
+ " 2026-06-01 | \n",
+ " 1108.681096 | \n",
+ " 2026-07-13 | \n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ " timestamp value released_at\n",
+ "409 2026-02-01 1033.768394 2026-03-24\n",
+ "410 2026-03-01 1121.177897 2026-04-15\n",
+ "411 2026-04-01 1137.410862 2026-06-05\n",
+ "412 2026-05-01 1130.026757 2026-06-05\n",
+ "413 2026-06-01 1108.681096 2026-07-13"
+ ]
+ },
+ "execution_count": 1,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "from __future__ import annotations\n",
+ "\n",
+ "import sys\n",
+ "from datetime import datetime, timezone\n",
+ "from pathlib import Path\n",
+ "\n",
+ "import pandas as pd\n",
+ "from dotenv import load_dotenv\n",
+ "\n",
+ "\n",
+ "ROOT = Path.cwd().resolve().parents[1]\n",
+ "sys.path.insert(0, str(ROOT / \"implementations\"))\n",
+ "load_dotenv(ROOT / \".env\")\n",
+ "\n",
+ "from pko.data import PALM_OIL_SERIES_ID, build_palm_oil_service\n",
+ "from pko.plots import (\n",
+ " DEFAULT_CUTOFFS,\n",
+ " plot_cutoff_windows,\n",
+ " plot_information_gap,\n",
+ " plot_monthly_changes,\n",
+ " plot_oil_complex,\n",
+ " plot_price_history,\n",
+ ")\n",
+ "\n",
+ "\n",
+ "svc = build_palm_oil_service(cache_dir=ROOT / \"data\" / \"fred\")\n",
+ "as_of = datetime.now(tz=timezone.utc).replace(tzinfo=None)\n",
+ "prices = svc.get_series(PALM_OIL_SERIES_ID, as_of=as_of)\n",
+ "\n",
+ "print(f\"{len(prices)} monthly observations\")\n",
+ "print(f\"span : {prices.timestamp.min():%Y-%m} -> {prices.timestamp.max():%Y-%m}\")\n",
+ "print(f\"price : ${prices.value.min():.0f} to ${prices.value.max():.0f} per tonne\")\n",
+ "prices.tail()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "58434dd4",
+ "metadata": {},
+ "source": [
+ "The `released_at` column is the point of this whole setup — it is the date FRED\n",
+ "*published* each price, not the month the price refers to. June 2026's price was\n",
+ "published on 2026-07-13, six weeks after the timestamp says.\n",
+ "\n",
+ "That column is what stops the harness handing a model a price that did not exist yet.\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b389d408",
+ "metadata": {},
+ "source": [
+ "---\n",
+ "## 2. The signal itself\n",
+ "\n",
+ "Full history since 2015, with the 7 candidate cutoffs marked and the two publication\n",
+ "blackouts shaded in red. Drag the range slider at the bottom to zoom into any period.\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "4de7760f",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-08-06T19:18:02.198528Z",
+ "iopub.status.busy": "2026-08-06T19:18:02.197812Z",
+ "iopub.status.idle": "2026-08-06T19:18:04.437350Z",
+ "shell.execute_reply": "2026-08-06T19:18:04.435861Z"
+ }
+ },
+ "outputs": [
+ {
+ "data": {
+ "application/vnd.plotly.v1+json": {
+ "config": {
+ "plotlyServerURL": "https://plot.ly"
+ },
+ "data": [
+ {
+ "hovertemplate": "%{x|%b %Y}
$%{y:.0f}/tonne",
+ "line": {
+ "color": "#2a78d6",
+ "width": 2
+ },
+ "mode": "lines",
+ "name": "Palm oil",
+ "type": "scatter",
+ "x": [
+ "2015-01-01T00:00:00.000000",
+ "2015-02-01T00:00:00.000000",
+ "2015-03-01T00:00:00.000000",
+ "2015-04-01T00:00:00.000000",
+ "2015-05-01T00:00:00.000000",
+ "2015-06-01T00:00:00.000000",
+ "2015-07-01T00:00:00.000000",
+ "2015-08-01T00:00:00.000000",
+ "2015-09-01T00:00:00.000000",
+ "2015-10-01T00:00:00.000000",
+ "2015-11-01T00:00:00.000000",
+ "2015-12-01T00:00:00.000000",
+ "2016-01-01T00:00:00.000000",
+ "2016-02-01T00:00:00.000000",
+ "2016-03-01T00:00:00.000000",
+ "2016-04-01T00:00:00.000000",
+ "2016-05-01T00:00:00.000000",
+ "2016-06-01T00:00:00.000000",
+ "2016-07-01T00:00:00.000000",
+ "2016-08-01T00:00:00.000000",
+ "2016-09-01T00:00:00.000000",
+ "2016-10-01T00:00:00.000000",
+ "2016-11-01T00:00:00.000000",
+ "2016-12-01T00:00:00.000000",
+ "2017-01-01T00:00:00.000000",
+ "2017-02-01T00:00:00.000000",
+ "2017-03-01T00:00:00.000000",
+ "2017-04-01T00:00:00.000000",
+ "2017-05-01T00:00:00.000000",
+ "2017-06-01T00:00:00.000000",
+ "2017-07-01T00:00:00.000000",
+ "2017-08-01T00:00:00.000000",
+ "2017-09-01T00:00:00.000000",
+ "2017-10-01T00:00:00.000000",
+ "2017-11-01T00:00:00.000000",
+ "2017-12-01T00:00:00.000000",
+ "2018-01-01T00:00:00.000000",
+ "2018-02-01T00:00:00.000000",
+ "2018-03-01T00:00:00.000000",
+ "2018-04-01T00:00:00.000000",
+ "2018-05-01T00:00:00.000000",
+ "2018-06-01T00:00:00.000000",
+ "2018-07-01T00:00:00.000000",
+ "2018-08-01T00:00:00.000000",
+ "2018-09-01T00:00:00.000000",
+ "2018-10-01T00:00:00.000000",
+ "2018-11-01T00:00:00.000000",
+ "2018-12-01T00:00:00.000000",
+ "2019-01-01T00:00:00.000000",
+ "2019-02-01T00:00:00.000000",
+ "2019-03-01T00:00:00.000000",
+ "2019-04-01T00:00:00.000000",
+ "2019-05-01T00:00:00.000000",
+ "2019-06-01T00:00:00.000000",
+ "2019-07-01T00:00:00.000000",
+ "2019-08-01T00:00:00.000000",
+ "2019-09-01T00:00:00.000000",
+ "2019-10-01T00:00:00.000000",
+ "2019-11-01T00:00:00.000000",
+ "2019-12-01T00:00:00.000000",
+ "2020-01-01T00:00:00.000000",
+ "2020-02-01T00:00:00.000000",
+ "2020-03-01T00:00:00.000000",
+ "2020-04-01T00:00:00.000000",
+ "2020-05-01T00:00:00.000000",
+ "2020-06-01T00:00:00.000000",
+ "2020-07-01T00:00:00.000000",
+ "2020-08-01T00:00:00.000000",
+ "2020-09-01T00:00:00.000000",
+ "2020-10-01T00:00:00.000000",
+ "2020-11-01T00:00:00.000000",
+ "2020-12-01T00:00:00.000000",
+ "2021-01-01T00:00:00.000000",
+ "2021-02-01T00:00:00.000000",
+ "2021-03-01T00:00:00.000000",
+ "2021-04-01T00:00:00.000000",
+ "2021-05-01T00:00:00.000000",
+ "2021-06-01T00:00:00.000000",
+ "2021-07-01T00:00:00.000000",
+ "2021-08-01T00:00:00.000000",
+ "2021-09-01T00:00:00.000000",
+ "2021-10-01T00:00:00.000000",
+ "2021-11-01T00:00:00.000000",
+ "2021-12-01T00:00:00.000000",
+ "2022-01-01T00:00:00.000000",
+ "2022-02-01T00:00:00.000000",
+ "2022-03-01T00:00:00.000000",
+ "2022-04-01T00:00:00.000000",
+ "2022-05-01T00:00:00.000000",
+ "2022-06-01T00:00:00.000000",
+ "2022-07-01T00:00:00.000000",
+ "2022-08-01T00:00:00.000000",
+ "2022-09-01T00:00:00.000000",
+ "2022-10-01T00:00:00.000000",
+ "2022-11-01T00:00:00.000000",
+ "2022-12-01T00:00:00.000000",
+ "2023-01-01T00:00:00.000000",
+ "2023-02-01T00:00:00.000000",
+ "2023-03-01T00:00:00.000000",
+ "2023-04-01T00:00:00.000000",
+ "2023-05-01T00:00:00.000000",
+ "2023-06-01T00:00:00.000000",
+ "2023-07-01T00:00:00.000000",
+ "2023-08-01T00:00:00.000000",
+ "2023-09-01T00:00:00.000000",
+ "2023-10-01T00:00:00.000000",
+ "2023-11-01T00:00:00.000000",
+ "2023-12-01T00:00:00.000000",
+ "2024-01-01T00:00:00.000000",
+ "2024-02-01T00:00:00.000000",
+ "2024-03-01T00:00:00.000000",
+ "2024-04-01T00:00:00.000000",
+ "2024-05-01T00:00:00.000000",
+ "2024-06-01T00:00:00.000000",
+ "2024-07-01T00:00:00.000000",
+ "2024-08-01T00:00:00.000000",
+ "2024-09-01T00:00:00.000000",
+ "2024-10-01T00:00:00.000000",
+ "2024-11-01T00:00:00.000000",
+ "2024-12-01T00:00:00.000000",
+ "2025-01-01T00:00:00.000000",
+ "2025-02-01T00:00:00.000000",
+ "2025-03-01T00:00:00.000000",
+ "2025-04-01T00:00:00.000000",
+ "2025-05-01T00:00:00.000000",
+ "2025-06-01T00:00:00.000000",
+ "2025-07-01T00:00:00.000000",
+ "2025-08-01T00:00:00.000000",
+ "2025-09-01T00:00:00.000000",
+ "2025-10-01T00:00:00.000000",
+ "2025-11-01T00:00:00.000000",
+ "2025-12-01T00:00:00.000000",
+ "2026-01-01T00:00:00.000000",
+ "2026-02-01T00:00:00.000000",
+ "2026-03-01T00:00:00.000000",
+ "2026-04-01T00:00:00.000000",
+ "2026-05-01T00:00:00.000000",
+ "2026-06-01T00:00:00.000000"
+ ],
+ "y": {
+ "bdata": "Wg8vIscMhECSxX5mBtODQAkoCio9/YJAwM8/qU1+gkBXmUQdLMuCQO0PdGQ884JAvaRiAHX9gUDDHP9p10p+QNlzGwPMN35ArDZIHvmRgEDqlwQwnnJ/QET/9g7SRIBAwbhOnvScgEAEXWiZNJ+CQJSLsNGKyINAvl0y/wBDhUABplSwhCSEQIx2W9CfU4NAbIZmZIVBgkC7Z0tXBMOEQLH8I9ZAo4VAfxn00ZZbhEBtGIoP+++EQFnNv8UMPoZAFY0LkOizhkBrn6xIWRaGQMIFa2pluoRAX6EnTKl5g0ASXwC+FXyEQMFm2uNzaYNAeAlJCURJg0AGwAVEjm6DQMmWCHINrYRAk5hBFktBhED/i/usgtyDQJGTGeNhhoJAw52vrsfKg0BAEVy4jBeEQABOcKp4goNAVFLEwwFQg0B/F5hSLPCCQO54pBUvN4JAqODLUqrRgEAvz/mv0rKAQJIfOSzTZoBAU6vGWLKtf0ASiUH/RsF7QPW5YM8F53xAdH5AOjk5gEBAxc2dTVKAQOhdQfzeyn5A/JAN0o90f0ACfEZrkIF9QGhaAXwIqn1Ae4dU/G4jfUDRBWMGvsl/QNlP1XezrX9Aob/YOilcgEDSFesWbCaDQBpJsWlEaoVARQsNUK+0hkCqQEpqBBKEQJ1XoAI5b4FAx7HSEcszgEDXe2fytid/QBEyvOfJ6YFA2gHX+/YTg0ACXWdzmBWFQLcrDy2aGoZAodS1PJnXhkARjNcSHk2KQA9VI+wvdYxA3K3d45hDjUAPvA9/8kaOQEJFaIpoHo9AUFgkdCwxkEBMPqj4VkKRQICtwoCzx4xAhxDuc8qRj0D09+VYH9eQQMRUx8fpR5FA90JQAbFYk0AU5RgxkP6TQGhT8Anb3ZJAgCw8gGE2lEC8sqZzR8iWQBbMEpbd0plAHsHYI0w0mUDZLSpv4wSZQAkQZA29qJRApo0iEiE3jECJoxxwmPKMQGXaUznp04hAXeuDGWnliEDaRqSMR12LQIHFOCXbuItAk0XPnks6jEASA3n/STGMQBR38oQ+cYxAfJV5Ono2jUCkH91FBxWKQGA2Ga7JCIhAGjsl7tBnikDaRpyK3byJQLJHyX6ytYhAHcYqxtn7h0CREhxI59+IQGbmFnHAeohAYEnfVep6iUDlwVZTtsiJQKgKD/RZGIxASBv+HvsGjEAgkWQpVNGJQKREHKLVMYpAaa9PZL7kikCjnM4qn8GLQKyzPabbdI1AVbAMHzUokEATdAP1fbORQCkubbMbn5FA9iYDBicYkEDNp2uYTK2QQDghZdeWhJBALJRYymupjkDkr9IqTjaMQF2aS/oGNI1AMI2kIlMYjUCCl5M2DQiQQJziUeCZK5BABxCaHyA5kEAmMK/uc4iOQOEV/h/MqY5Aiu5kDLxij0CYzNzVEieQQDduqiq2hJFAH8USuaTFkUBUwkdmG6iRQLPAKHG5UpFA",
+ "dtype": "f8"
+ }
+ }
+ ],
+ "layout": {
+ "annotations": [
+ {
+ "font": {
+ "size": 10
+ },
+ "showarrow": false,
+ "text": "no data published",
+ "x": "2021-12-01",
+ "xanchor": "left",
+ "xref": "x",
+ "y": 1,
+ "yanchor": "top",
+ "yref": "y domain"
+ },
+ {
+ "font": {
+ "size": 10
+ },
+ "showarrow": false,
+ "x": "2025-07-01",
+ "xanchor": "left",
+ "xref": "x",
+ "y": 1,
+ "yanchor": "top",
+ "yref": "y domain"
+ },
+ {
+ "font": {
+ "size": 9
+ },
+ "showarrow": false,
+ "text": "2021-05 (event)",
+ "x": "2021-05-01T00:00:00",
+ "xanchor": "center",
+ "xref": "x",
+ "y": 1,
+ "yanchor": "bottom",
+ "yref": "y domain"
+ },
+ {
+ "font": {
+ "size": 9
+ },
+ "showarrow": false,
+ "text": "2022-01 (event)",
+ "x": "2022-01-01T00:00:00",
+ "xanchor": "center",
+ "xref": "x",
+ "y": 1,
+ "yanchor": "bottom",
+ "yref": "y domain"
+ },
+ {
+ "font": {
+ "size": 9
+ },
+ "showarrow": false,
+ "text": "2023-04 (event)",
+ "x": "2023-04-01T00:00:00",
+ "xanchor": "center",
+ "xref": "x",
+ "y": 1,
+ "yanchor": "bottom",
+ "yref": "y domain"
+ },
+ {
+ "font": {
+ "size": 9
+ },
+ "showarrow": false,
+ "text": "2024-09 (event)",
+ "x": "2024-09-01T00:00:00",
+ "xanchor": "center",
+ "xref": "x",
+ "y": 1,
+ "yanchor": "bottom",
+ "yref": "y domain"
+ },
+ {
+ "font": {
+ "size": 9
+ },
+ "showarrow": false,
+ "text": "2023-07 (quiet)",
+ "x": "2023-07-01T00:00:00",
+ "xanchor": "center",
+ "xref": "x",
+ "y": 1,
+ "yanchor": "bottom",
+ "yref": "y domain"
+ },
+ {
+ "font": {
+ "size": 9
+ },
+ "showarrow": false,
+ "text": "2024-11 (quiet)",
+ "x": "2024-11-01T00:00:00",
+ "xanchor": "center",
+ "xref": "x",
+ "y": 1,
+ "yanchor": "bottom",
+ "yref": "y domain"
+ },
+ {
+ "font": {
+ "size": 9
+ },
+ "showarrow": false,
+ "text": "2025-08 (quiet)",
+ "x": "2025-08-01T00:00:00",
+ "xanchor": "center",
+ "xref": "x",
+ "y": 1,
+ "yanchor": "bottom",
+ "yref": "y domain"
+ }
+ ],
+ "font": {
+ "color": "#52514e",
+ "size": 12
+ },
+ "height": 520,
+ "hovermode": "x unified",
+ "legend": {
+ "orientation": "h",
+ "x": 0,
+ "xanchor": "left",
+ "y": 1.02,
+ "yanchor": "bottom"
+ },
+ "margin": {
+ "b": 55,
+ "l": 70,
+ "r": 110,
+ "t": 70
+ },
+ "paper_bgcolor": "#fcfcfb",
+ "plot_bgcolor": "#fcfcfb",
+ "shapes": [
+ {
+ "fillcolor": "rgba(227, 73, 72, 0.10)",
+ "layer": "below",
+ "line": {
+ "width": 0
+ },
+ "type": "rect",
+ "x0": "2021-12-01",
+ "x1": "2022-08-01",
+ "xref": "x",
+ "y0": 0,
+ "y1": 1,
+ "yref": "y domain"
+ },
+ {
+ "fillcolor": "rgba(227, 73, 72, 0.10)",
+ "layer": "below",
+ "line": {
+ "width": 0
+ },
+ "type": "rect",
+ "x0": "2025-07-01",
+ "x1": "2026-01-01",
+ "xref": "x",
+ "y0": 0,
+ "y1": 1,
+ "yref": "y domain"
+ },
+ {
+ "line": {
+ "color": "#52514e",
+ "dash": "dot",
+ "width": 1
+ },
+ "type": "line",
+ "x0": "2021-05-01T00:00:00",
+ "x1": "2021-05-01T00:00:00",
+ "xref": "x",
+ "y0": 0,
+ "y1": 1,
+ "yref": "y domain"
+ },
+ {
+ "line": {
+ "color": "#52514e",
+ "dash": "dot",
+ "width": 1
+ },
+ "type": "line",
+ "x0": "2022-01-01T00:00:00",
+ "x1": "2022-01-01T00:00:00",
+ "xref": "x",
+ "y0": 0,
+ "y1": 1,
+ "yref": "y domain"
+ },
+ {
+ "line": {
+ "color": "#52514e",
+ "dash": "dot",
+ "width": 1
+ },
+ "type": "line",
+ "x0": "2023-04-01T00:00:00",
+ "x1": "2023-04-01T00:00:00",
+ "xref": "x",
+ "y0": 0,
+ "y1": 1,
+ "yref": "y domain"
+ },
+ {
+ "line": {
+ "color": "#52514e",
+ "dash": "dot",
+ "width": 1
+ },
+ "type": "line",
+ "x0": "2024-09-01T00:00:00",
+ "x1": "2024-09-01T00:00:00",
+ "xref": "x",
+ "y0": 0,
+ "y1": 1,
+ "yref": "y domain"
+ },
+ {
+ "line": {
+ "color": "#52514e",
+ "dash": "dot",
+ "width": 1
+ },
+ "type": "line",
+ "x0": "2023-07-01T00:00:00",
+ "x1": "2023-07-01T00:00:00",
+ "xref": "x",
+ "y0": 0,
+ "y1": 1,
+ "yref": "y domain"
+ },
+ {
+ "line": {
+ "color": "#52514e",
+ "dash": "dot",
+ "width": 1
+ },
+ "type": "line",
+ "x0": "2024-11-01T00:00:00",
+ "x1": "2024-11-01T00:00:00",
+ "xref": "x",
+ "y0": 0,
+ "y1": 1,
+ "yref": "y domain"
+ },
+ {
+ "line": {
+ "color": "#52514e",
+ "dash": "dot",
+ "width": 1
+ },
+ "type": "line",
+ "x0": "2025-08-01T00:00:00",
+ "x1": "2025-08-01T00:00:00",
+ "xref": "x",
+ "y0": 0,
+ "y1": 1,
+ "yref": "y domain"
+ }
+ ],
+ "template": {
+ "data": {
+ "bar": [
+ {
+ "error_x": {
+ "color": "#2a3f5f"
+ },
+ "error_y": {
+ "color": "#2a3f5f"
+ },
+ "marker": {
+ "line": {
+ "color": "#E5ECF6",
+ "width": 0.5
+ },
+ "pattern": {
+ "fillmode": "overlay",
+ "size": 10,
+ "solidity": 0.2
+ }
+ },
+ "type": "bar"
+ }
+ ],
+ "barpolar": [
+ {
+ "marker": {
+ "line": {
+ "color": "#E5ECF6",
+ "width": 0.5
+ },
+ "pattern": {
+ "fillmode": "overlay",
+ "size": 10,
+ "solidity": 0.2
+ }
+ },
+ "type": "barpolar"
+ }
+ ],
+ "carpet": [
+ {
+ "aaxis": {
+ "endlinecolor": "#2a3f5f",
+ "gridcolor": "white",
+ "linecolor": "white",
+ "minorgridcolor": "white",
+ "startlinecolor": "#2a3f5f"
+ },
+ "baxis": {
+ "endlinecolor": "#2a3f5f",
+ "gridcolor": "white",
+ "linecolor": "white",
+ "minorgridcolor": "white",
+ "startlinecolor": "#2a3f5f"
+ },
+ "type": "carpet"
+ }
+ ],
+ "choropleth": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "type": "choropleth"
+ }
+ ],
+ "contour": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "colorscale": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "type": "contour"
+ }
+ ],
+ "contourcarpet": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "type": "contourcarpet"
+ }
+ ],
+ "heatmap": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "colorscale": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "type": "heatmap"
+ }
+ ],
+ "histogram": [
+ {
+ "marker": {
+ "pattern": {
+ "fillmode": "overlay",
+ "size": 10,
+ "solidity": 0.2
+ }
+ },
+ "type": "histogram"
+ }
+ ],
+ "histogram2d": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "colorscale": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "type": "histogram2d"
+ }
+ ],
+ "histogram2dcontour": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "colorscale": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "type": "histogram2dcontour"
+ }
+ ],
+ "mesh3d": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "type": "mesh3d"
+ }
+ ],
+ "parcoords": [
+ {
+ "line": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "parcoords"
+ }
+ ],
+ "pie": [
+ {
+ "automargin": true,
+ "type": "pie"
+ }
+ ],
+ "scatter": [
+ {
+ "fillpattern": {
+ "fillmode": "overlay",
+ "size": 10,
+ "solidity": 0.2
+ },
+ "type": "scatter"
+ }
+ ],
+ "scatter3d": [
+ {
+ "line": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scatter3d"
+ }
+ ],
+ "scattercarpet": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scattercarpet"
+ }
+ ],
+ "scattergeo": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scattergeo"
+ }
+ ],
+ "scattergl": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scattergl"
+ }
+ ],
+ "scattermap": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scattermap"
+ }
+ ],
+ "scattermapbox": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scattermapbox"
+ }
+ ],
+ "scatterpolar": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scatterpolar"
+ }
+ ],
+ "scatterpolargl": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scatterpolargl"
+ }
+ ],
+ "scatterternary": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scatterternary"
+ }
+ ],
+ "surface": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "colorscale": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "type": "surface"
+ }
+ ],
+ "table": [
+ {
+ "cells": {
+ "fill": {
+ "color": "#EBF0F8"
+ },
+ "line": {
+ "color": "white"
+ }
+ },
+ "header": {
+ "fill": {
+ "color": "#C8D4E3"
+ },
+ "line": {
+ "color": "white"
+ }
+ },
+ "type": "table"
+ }
+ ]
+ },
+ "layout": {
+ "annotationdefaults": {
+ "arrowcolor": "#2a3f5f",
+ "arrowhead": 0,
+ "arrowwidth": 1
+ },
+ "autotypenumbers": "strict",
+ "coloraxis": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "colorscale": {
+ "diverging": [
+ [
+ 0,
+ "#8e0152"
+ ],
+ [
+ 0.1,
+ "#c51b7d"
+ ],
+ [
+ 0.2,
+ "#de77ae"
+ ],
+ [
+ 0.3,
+ "#f1b6da"
+ ],
+ [
+ 0.4,
+ "#fde0ef"
+ ],
+ [
+ 0.5,
+ "#f7f7f7"
+ ],
+ [
+ 0.6,
+ "#e6f5d0"
+ ],
+ [
+ 0.7,
+ "#b8e186"
+ ],
+ [
+ 0.8,
+ "#7fbc41"
+ ],
+ [
+ 0.9,
+ "#4d9221"
+ ],
+ [
+ 1,
+ "#276419"
+ ]
+ ],
+ "sequential": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "sequentialminus": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ]
+ },
+ "colorway": [
+ "#636efa",
+ "#EF553B",
+ "#00cc96",
+ "#ab63fa",
+ "#FFA15A",
+ "#19d3f3",
+ "#FF6692",
+ "#B6E880",
+ "#FF97FF",
+ "#FECB52"
+ ],
+ "font": {
+ "color": "#2a3f5f"
+ },
+ "geo": {
+ "bgcolor": "white",
+ "lakecolor": "white",
+ "landcolor": "#E5ECF6",
+ "showlakes": true,
+ "showland": true,
+ "subunitcolor": "white"
+ },
+ "hoverlabel": {
+ "align": "left"
+ },
+ "hovermode": "closest",
+ "mapbox": {
+ "style": "light"
+ },
+ "paper_bgcolor": "white",
+ "plot_bgcolor": "#E5ECF6",
+ "polar": {
+ "angularaxis": {
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": ""
+ },
+ "bgcolor": "#E5ECF6",
+ "radialaxis": {
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": ""
+ }
+ },
+ "scene": {
+ "xaxis": {
+ "backgroundcolor": "#E5ECF6",
+ "gridcolor": "white",
+ "gridwidth": 2,
+ "linecolor": "white",
+ "showbackground": true,
+ "ticks": "",
+ "zerolinecolor": "white"
+ },
+ "yaxis": {
+ "backgroundcolor": "#E5ECF6",
+ "gridcolor": "white",
+ "gridwidth": 2,
+ "linecolor": "white",
+ "showbackground": true,
+ "ticks": "",
+ "zerolinecolor": "white"
+ },
+ "zaxis": {
+ "backgroundcolor": "#E5ECF6",
+ "gridcolor": "white",
+ "gridwidth": 2,
+ "linecolor": "white",
+ "showbackground": true,
+ "ticks": "",
+ "zerolinecolor": "white"
+ }
+ },
+ "shapedefaults": {
+ "line": {
+ "color": "#2a3f5f"
+ }
+ },
+ "ternary": {
+ "aaxis": {
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": ""
+ },
+ "baxis": {
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": ""
+ },
+ "bgcolor": "#E5ECF6",
+ "caxis": {
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": ""
+ }
+ },
+ "title": {
+ "x": 0.05
+ },
+ "xaxis": {
+ "automargin": true,
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": "",
+ "title": {
+ "standoff": 15
+ },
+ "zerolinecolor": "white",
+ "zerolinewidth": 2
+ },
+ "yaxis": {
+ "automargin": true,
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": "",
+ "title": {
+ "standoff": 15
+ },
+ "zerolinecolor": "white",
+ "zerolinewidth": 2
+ }
+ }
+ },
+ "title": {
+ "font": {
+ "color": "#0b0b0b",
+ "size": 17
+ },
+ "text": "Palm oil price (FRED PPOILUSDM)"
+ },
+ "xaxis": {
+ "gridcolor": "#e6e5e1",
+ "linecolor": "#e6e5e1",
+ "rangeslider": {
+ "thickness": 0.06,
+ "visible": true
+ },
+ "showspikes": false,
+ "zeroline": false
+ },
+ "yaxis": {
+ "gridcolor": "#e6e5e1",
+ "linecolor": "#e6e5e1",
+ "showspikes": false,
+ "title": {
+ "text": "USD / metric ton"
+ },
+ "zeroline": false
+ }
+ }
+ }
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "plot_price_history(prices, cutoffs=DEFAULT_CUTOFFS)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "c3ca1611",
+ "metadata": {},
+ "source": [
+ "Things to look for:\n",
+ "\n",
+ "- The **2021–2022 spike** to \\$1,653 and the crash back to \\$903 — the Indonesian export ban.\n",
+ "- The **red bands** are periods when FRED published nothing at all. Note that the first\n",
+ " one covers the entire export-ban episode.\n",
+ "- The recent climb through 2026 to around \\$1,100.\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "a1e0ea83",
+ "metadata": {},
+ "source": [
+ "---\n",
+ "## 3. Month-over-month change\n",
+ "\n",
+ "The same series as returns, which is what a forecaster is really trying to predict.\n",
+ "Blue is up, red is down.\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "f991dd0b",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-08-06T19:18:04.440836Z",
+ "iopub.status.busy": "2026-08-06T19:18:04.440483Z",
+ "iopub.status.idle": "2026-08-06T19:18:04.485793Z",
+ "shell.execute_reply": "2026-08-06T19:18:04.483963Z"
+ }
+ },
+ "outputs": [
+ {
+ "data": {
+ "application/vnd.plotly.v1+json": {
+ "config": {
+ "plotlyServerURL": "https://plot.ly"
+ },
+ "data": [
+ {
+ "hovertemplate": "%{x|%b %Y}
%{y:+.1f}%",
+ "marker": {
+ "color": [
+ "#2a78d6",
+ "#e34948",
+ "#e34948",
+ "#e34948",
+ "#e34948",
+ "#2a78d6",
+ "#2a78d6",
+ "#2a78d6",
+ "#2a78d6",
+ "#2a78d6",
+ "#2a78d6",
+ "#2a78d6",
+ "#2a78d6",
+ "#2a78d6",
+ "#2a78d6",
+ "#2a78d6",
+ "#2a78d6",
+ "#e34948",
+ "#2a78d6",
+ "#2a78d6",
+ "#2a78d6",
+ "#2a78d6",
+ "#2a78d6",
+ "#e34948",
+ "#2a78d6",
+ "#2a78d6",
+ "#2a78d6",
+ "#e34948",
+ "#e34948",
+ "#e34948",
+ "#e34948",
+ "#2a78d6",
+ "#e34948",
+ "#2a78d6",
+ "#2a78d6",
+ "#2a78d6",
+ "#2a78d6",
+ "#e34948",
+ "#2a78d6",
+ "#2a78d6",
+ "#e34948",
+ "#e34948",
+ "#2a78d6",
+ "#e34948",
+ "#e34948",
+ "#e34948",
+ "#2a78d6",
+ "#e34948",
+ "#2a78d6",
+ "#2a78d6",
+ "#2a78d6",
+ "#e34948",
+ "#e34948",
+ "#2a78d6",
+ "#2a78d6",
+ "#2a78d6",
+ "#2a78d6",
+ "#2a78d6",
+ "#2a78d6",
+ "#e34948",
+ "#e34948",
+ "#2a78d6",
+ "#e34948",
+ "#e34948",
+ "#e34948",
+ "#2a78d6",
+ "#e34948",
+ "#2a78d6",
+ "#2a78d6",
+ "#2a78d6",
+ "#e34948",
+ "#2a78d6",
+ "#2a78d6",
+ "#2a78d6",
+ "#2a78d6",
+ "#2a78d6",
+ "#e34948",
+ "#e34948"
+ ]
+ },
+ "name": "Monthly change",
+ "showlegend": false,
+ "type": "bar",
+ "x": [
+ "2020-01-01T00:00:00.000000",
+ "2020-02-01T00:00:00.000000",
+ "2020-03-01T00:00:00.000000",
+ "2020-04-01T00:00:00.000000",
+ "2020-05-01T00:00:00.000000",
+ "2020-06-01T00:00:00.000000",
+ "2020-07-01T00:00:00.000000",
+ "2020-08-01T00:00:00.000000",
+ "2020-09-01T00:00:00.000000",
+ "2020-10-01T00:00:00.000000",
+ "2020-11-01T00:00:00.000000",
+ "2020-12-01T00:00:00.000000",
+ "2021-01-01T00:00:00.000000",
+ "2021-02-01T00:00:00.000000",
+ "2021-03-01T00:00:00.000000",
+ "2021-04-01T00:00:00.000000",
+ "2021-05-01T00:00:00.000000",
+ "2021-06-01T00:00:00.000000",
+ "2021-07-01T00:00:00.000000",
+ "2021-08-01T00:00:00.000000",
+ "2021-09-01T00:00:00.000000",
+ "2021-10-01T00:00:00.000000",
+ "2021-11-01T00:00:00.000000",
+ "2021-12-01T00:00:00.000000",
+ "2022-01-01T00:00:00.000000",
+ "2022-02-01T00:00:00.000000",
+ "2022-03-01T00:00:00.000000",
+ "2022-04-01T00:00:00.000000",
+ "2022-05-01T00:00:00.000000",
+ "2022-06-01T00:00:00.000000",
+ "2022-07-01T00:00:00.000000",
+ "2022-08-01T00:00:00.000000",
+ "2022-09-01T00:00:00.000000",
+ "2022-10-01T00:00:00.000000",
+ "2022-11-01T00:00:00.000000",
+ "2022-12-01T00:00:00.000000",
+ "2023-01-01T00:00:00.000000",
+ "2023-02-01T00:00:00.000000",
+ "2023-03-01T00:00:00.000000",
+ "2023-04-01T00:00:00.000000",
+ "2023-05-01T00:00:00.000000",
+ "2023-06-01T00:00:00.000000",
+ "2023-07-01T00:00:00.000000",
+ "2023-08-01T00:00:00.000000",
+ "2023-09-01T00:00:00.000000",
+ "2023-10-01T00:00:00.000000",
+ "2023-11-01T00:00:00.000000",
+ "2023-12-01T00:00:00.000000",
+ "2024-01-01T00:00:00.000000",
+ "2024-02-01T00:00:00.000000",
+ "2024-03-01T00:00:00.000000",
+ "2024-04-01T00:00:00.000000",
+ "2024-05-01T00:00:00.000000",
+ "2024-06-01T00:00:00.000000",
+ "2024-07-01T00:00:00.000000",
+ "2024-08-01T00:00:00.000000",
+ "2024-09-01T00:00:00.000000",
+ "2024-10-01T00:00:00.000000",
+ "2024-11-01T00:00:00.000000",
+ "2024-12-01T00:00:00.000000",
+ "2025-01-01T00:00:00.000000",
+ "2025-02-01T00:00:00.000000",
+ "2025-03-01T00:00:00.000000",
+ "2025-04-01T00:00:00.000000",
+ "2025-05-01T00:00:00.000000",
+ "2025-06-01T00:00:00.000000",
+ "2025-07-01T00:00:00.000000",
+ "2025-08-01T00:00:00.000000",
+ "2025-09-01T00:00:00.000000",
+ "2025-10-01T00:00:00.000000",
+ "2025-11-01T00:00:00.000000",
+ "2025-12-01T00:00:00.000000",
+ "2026-01-01T00:00:00.000000",
+ "2026-02-01T00:00:00.000000",
+ "2026-03-01T00:00:00.000000",
+ "2026-04-01T00:00:00.000000",
+ "2026-05-01T00:00:00.000000",
+ "2026-06-01T00:00:00.000000"
+ ],
+ "y": {
+ "bdata": "zi+0QKwbGEC8PfZxsDYnwJ7rbWVIRCrAYpH4++pEHMBXSrPM8NgOwLZNUm7T/C1AGLj1oToCGkBUqhyziQglQNcW7X2qVxNA1pLm0Ei4CkC4vQh9XUkuQPgprgYNZiBAshb0tJCqBkB6RcaR7bELQDYzTasJPQZAfSnqAedDEEBfXVmHI1waQOx0iD2nnzDAC8l+QmNiI0DKjh0Zzb8aQAzNj80g7gRAocwjssvnJ0A+d9QBAssKQBRsA/rFjxbAVrPRVXCIHECwZCr51m0pQIbk2vb/sipAl8dun1QwA8BUdQfdKoPnv9mxjDMxbTHAs7j99lC2P8A+3qCgTMMEQOr9kbXXdizAIP7rg/Oe0T88qhNdCtQjQKCn529/6vQ/6OpByK4u/T9gH4Ip8Oe/v3D8xK4+W+w/JOdOYZerBUBnfZim+G4lwMC6N+3WZx/ARncxKFi7I0AFwLF5PTsEwPGyetD88w/AvBNFGvKAB8CKyoFl5bYNQMiUw/9Wavm/f5vOJMRZEECAPnTXLBXzP2qQnMIw7SFAwCgZ+eLpzr8VR0of4ogfwBRCRHLAXPc/LIn4N/1XBUAsiMKPcaoJQDQ1JXI+gBhAVGi1ZglnI0Ajwz5tBh0jQMDsPoLxydy/YLEgK0FVIcB0NIeHn/UMQDy7aO1+g+6/bzh14MK+HMC4U793EfUfwLKH56awGgxAGBB9ahy317+00U1BvWYkQFjbyb/xt+s/AKqmL8jo1D+clL9DQ5YXwHDKiwhQTds/KnBCpfvYAkB6F7VUgnAHQLDfGl0t6SBA2Jbj6WMq9z848tlERcbkv8CKB5olOf6/",
+ "dtype": "f8"
+ }
+ }
+ ],
+ "layout": {
+ "annotations": [
+ {
+ "font": {
+ "size": 10
+ },
+ "showarrow": false,
+ "text": "no data published",
+ "x": "2021-12-01",
+ "xanchor": "left",
+ "xref": "x",
+ "y": 1,
+ "yanchor": "top",
+ "yref": "y domain"
+ },
+ {
+ "font": {
+ "size": 10
+ },
+ "showarrow": false,
+ "x": "2025-07-01",
+ "xanchor": "left",
+ "xref": "x",
+ "y": 1,
+ "yanchor": "top",
+ "yref": "y domain"
+ }
+ ],
+ "font": {
+ "color": "#52514e",
+ "size": 12
+ },
+ "height": 420,
+ "hovermode": "x unified",
+ "legend": {
+ "orientation": "h",
+ "x": 0,
+ "xanchor": "left",
+ "y": 1.02,
+ "yanchor": "bottom"
+ },
+ "margin": {
+ "b": 55,
+ "l": 70,
+ "r": 110,
+ "t": 70
+ },
+ "paper_bgcolor": "#fcfcfb",
+ "plot_bgcolor": "#fcfcfb",
+ "shapes": [
+ {
+ "fillcolor": "rgba(227, 73, 72, 0.10)",
+ "layer": "below",
+ "line": {
+ "width": 0
+ },
+ "type": "rect",
+ "x0": "2021-12-01",
+ "x1": "2022-08-01",
+ "xref": "x",
+ "y0": 0,
+ "y1": 1,
+ "yref": "y domain"
+ },
+ {
+ "fillcolor": "rgba(227, 73, 72, 0.10)",
+ "layer": "below",
+ "line": {
+ "width": 0
+ },
+ "type": "rect",
+ "x0": "2025-07-01",
+ "x1": "2026-01-01",
+ "xref": "x",
+ "y0": 0,
+ "y1": 1,
+ "yref": "y domain"
+ },
+ {
+ "line": {
+ "color": "#52514e",
+ "width": 1
+ },
+ "type": "line",
+ "x0": 0,
+ "x1": 1,
+ "xref": "x domain",
+ "y0": 0,
+ "y1": 0,
+ "yref": "y"
+ }
+ ],
+ "template": {
+ "data": {
+ "bar": [
+ {
+ "error_x": {
+ "color": "#2a3f5f"
+ },
+ "error_y": {
+ "color": "#2a3f5f"
+ },
+ "marker": {
+ "line": {
+ "color": "#E5ECF6",
+ "width": 0.5
+ },
+ "pattern": {
+ "fillmode": "overlay",
+ "size": 10,
+ "solidity": 0.2
+ }
+ },
+ "type": "bar"
+ }
+ ],
+ "barpolar": [
+ {
+ "marker": {
+ "line": {
+ "color": "#E5ECF6",
+ "width": 0.5
+ },
+ "pattern": {
+ "fillmode": "overlay",
+ "size": 10,
+ "solidity": 0.2
+ }
+ },
+ "type": "barpolar"
+ }
+ ],
+ "carpet": [
+ {
+ "aaxis": {
+ "endlinecolor": "#2a3f5f",
+ "gridcolor": "white",
+ "linecolor": "white",
+ "minorgridcolor": "white",
+ "startlinecolor": "#2a3f5f"
+ },
+ "baxis": {
+ "endlinecolor": "#2a3f5f",
+ "gridcolor": "white",
+ "linecolor": "white",
+ "minorgridcolor": "white",
+ "startlinecolor": "#2a3f5f"
+ },
+ "type": "carpet"
+ }
+ ],
+ "choropleth": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "type": "choropleth"
+ }
+ ],
+ "contour": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "colorscale": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "type": "contour"
+ }
+ ],
+ "contourcarpet": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "type": "contourcarpet"
+ }
+ ],
+ "heatmap": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "colorscale": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "type": "heatmap"
+ }
+ ],
+ "histogram": [
+ {
+ "marker": {
+ "pattern": {
+ "fillmode": "overlay",
+ "size": 10,
+ "solidity": 0.2
+ }
+ },
+ "type": "histogram"
+ }
+ ],
+ "histogram2d": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "colorscale": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "type": "histogram2d"
+ }
+ ],
+ "histogram2dcontour": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "colorscale": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "type": "histogram2dcontour"
+ }
+ ],
+ "mesh3d": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "type": "mesh3d"
+ }
+ ],
+ "parcoords": [
+ {
+ "line": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "parcoords"
+ }
+ ],
+ "pie": [
+ {
+ "automargin": true,
+ "type": "pie"
+ }
+ ],
+ "scatter": [
+ {
+ "fillpattern": {
+ "fillmode": "overlay",
+ "size": 10,
+ "solidity": 0.2
+ },
+ "type": "scatter"
+ }
+ ],
+ "scatter3d": [
+ {
+ "line": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scatter3d"
+ }
+ ],
+ "scattercarpet": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scattercarpet"
+ }
+ ],
+ "scattergeo": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scattergeo"
+ }
+ ],
+ "scattergl": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scattergl"
+ }
+ ],
+ "scattermap": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scattermap"
+ }
+ ],
+ "scattermapbox": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scattermapbox"
+ }
+ ],
+ "scatterpolar": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scatterpolar"
+ }
+ ],
+ "scatterpolargl": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scatterpolargl"
+ }
+ ],
+ "scatterternary": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scatterternary"
+ }
+ ],
+ "surface": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "colorscale": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "type": "surface"
+ }
+ ],
+ "table": [
+ {
+ "cells": {
+ "fill": {
+ "color": "#EBF0F8"
+ },
+ "line": {
+ "color": "white"
+ }
+ },
+ "header": {
+ "fill": {
+ "color": "#C8D4E3"
+ },
+ "line": {
+ "color": "white"
+ }
+ },
+ "type": "table"
+ }
+ ]
+ },
+ "layout": {
+ "annotationdefaults": {
+ "arrowcolor": "#2a3f5f",
+ "arrowhead": 0,
+ "arrowwidth": 1
+ },
+ "autotypenumbers": "strict",
+ "coloraxis": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "colorscale": {
+ "diverging": [
+ [
+ 0,
+ "#8e0152"
+ ],
+ [
+ 0.1,
+ "#c51b7d"
+ ],
+ [
+ 0.2,
+ "#de77ae"
+ ],
+ [
+ 0.3,
+ "#f1b6da"
+ ],
+ [
+ 0.4,
+ "#fde0ef"
+ ],
+ [
+ 0.5,
+ "#f7f7f7"
+ ],
+ [
+ 0.6,
+ "#e6f5d0"
+ ],
+ [
+ 0.7,
+ "#b8e186"
+ ],
+ [
+ 0.8,
+ "#7fbc41"
+ ],
+ [
+ 0.9,
+ "#4d9221"
+ ],
+ [
+ 1,
+ "#276419"
+ ]
+ ],
+ "sequential": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "sequentialminus": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ]
+ },
+ "colorway": [
+ "#636efa",
+ "#EF553B",
+ "#00cc96",
+ "#ab63fa",
+ "#FFA15A",
+ "#19d3f3",
+ "#FF6692",
+ "#B6E880",
+ "#FF97FF",
+ "#FECB52"
+ ],
+ "font": {
+ "color": "#2a3f5f"
+ },
+ "geo": {
+ "bgcolor": "white",
+ "lakecolor": "white",
+ "landcolor": "#E5ECF6",
+ "showlakes": true,
+ "showland": true,
+ "subunitcolor": "white"
+ },
+ "hoverlabel": {
+ "align": "left"
+ },
+ "hovermode": "closest",
+ "mapbox": {
+ "style": "light"
+ },
+ "paper_bgcolor": "white",
+ "plot_bgcolor": "#E5ECF6",
+ "polar": {
+ "angularaxis": {
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": ""
+ },
+ "bgcolor": "#E5ECF6",
+ "radialaxis": {
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": ""
+ }
+ },
+ "scene": {
+ "xaxis": {
+ "backgroundcolor": "#E5ECF6",
+ "gridcolor": "white",
+ "gridwidth": 2,
+ "linecolor": "white",
+ "showbackground": true,
+ "ticks": "",
+ "zerolinecolor": "white"
+ },
+ "yaxis": {
+ "backgroundcolor": "#E5ECF6",
+ "gridcolor": "white",
+ "gridwidth": 2,
+ "linecolor": "white",
+ "showbackground": true,
+ "ticks": "",
+ "zerolinecolor": "white"
+ },
+ "zaxis": {
+ "backgroundcolor": "#E5ECF6",
+ "gridcolor": "white",
+ "gridwidth": 2,
+ "linecolor": "white",
+ "showbackground": true,
+ "ticks": "",
+ "zerolinecolor": "white"
+ }
+ },
+ "shapedefaults": {
+ "line": {
+ "color": "#2a3f5f"
+ }
+ },
+ "ternary": {
+ "aaxis": {
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": ""
+ },
+ "baxis": {
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": ""
+ },
+ "bgcolor": "#E5ECF6",
+ "caxis": {
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": ""
+ }
+ },
+ "title": {
+ "x": 0.05
+ },
+ "xaxis": {
+ "automargin": true,
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": "",
+ "title": {
+ "standoff": 15
+ },
+ "zerolinecolor": "white",
+ "zerolinewidth": 2
+ },
+ "yaxis": {
+ "automargin": true,
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": "",
+ "title": {
+ "standoff": 15
+ },
+ "zerolinecolor": "white",
+ "zerolinewidth": 2
+ }
+ }
+ },
+ "title": {
+ "font": {
+ "color": "#0b0b0b",
+ "size": 17
+ },
+ "text": "Palm oil, month-over-month change"
+ },
+ "xaxis": {
+ "gridcolor": "#e6e5e1",
+ "linecolor": "#e6e5e1",
+ "showspikes": false,
+ "zeroline": false
+ },
+ "yaxis": {
+ "gridcolor": "#e6e5e1",
+ "linecolor": "#e6e5e1",
+ "showspikes": false,
+ "title": {
+ "text": "% change"
+ },
+ "zeroline": false
+ }
+ }
+ }
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "plot_monthly_changes(prices, start=\"2020-01-01\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "8e994d5a",
+ "metadata": {},
+ "source": [
+ "July 2022 is **−31.7%** — the largest single month in the data. It sits inside a\n",
+ "blackout, so nobody could see it happening at the time.\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "dadc6dab",
+ "metadata": {},
+ "source": [
+ "---\n",
+ "## 4. Do the candidate cutoffs actually look right?\n",
+ "\n",
+ "Each shaded band is one cutoff's 6-month forecast window. Orange bands are the\n",
+ "\"event\" cutoffs, blue are \"quiet\".\n",
+ "\n",
+ "**This chart is the check on the cutoff choice.** An event window should visibly\n",
+ "contain a shock; a quiet window should look flat. If one doesn't, swap it.\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "82ec403c",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-08-06T19:18:04.490381Z",
+ "iopub.status.busy": "2026-08-06T19:18:04.489991Z",
+ "iopub.status.idle": "2026-08-06T19:18:04.569579Z",
+ "shell.execute_reply": "2026-08-06T19:18:04.568497Z"
+ }
+ },
+ "outputs": [
+ {
+ "data": {
+ "application/vnd.plotly.v1+json": {
+ "config": {
+ "plotlyServerURL": "https://plot.ly"
+ },
+ "data": [
+ {
+ "hovertemplate": "%{x|%b %Y}
$%{y:.0f}/tonne",
+ "line": {
+ "color": "#2a78d6",
+ "width": 2
+ },
+ "mode": "lines",
+ "name": "Palm oil",
+ "type": "scatter",
+ "x": [
+ "2020-11-01T00:00:00.000000",
+ "2020-12-01T00:00:00.000000",
+ "2021-01-01T00:00:00.000000",
+ "2021-02-01T00:00:00.000000",
+ "2021-03-01T00:00:00.000000",
+ "2021-04-01T00:00:00.000000",
+ "2021-05-01T00:00:00.000000",
+ "2021-06-01T00:00:00.000000",
+ "2021-07-01T00:00:00.000000",
+ "2021-08-01T00:00:00.000000",
+ "2021-09-01T00:00:00.000000",
+ "2021-10-01T00:00:00.000000",
+ "2021-11-01T00:00:00.000000",
+ "2021-12-01T00:00:00.000000",
+ "2022-01-01T00:00:00.000000",
+ "2022-02-01T00:00:00.000000",
+ "2022-03-01T00:00:00.000000",
+ "2022-04-01T00:00:00.000000",
+ "2022-05-01T00:00:00.000000",
+ "2022-06-01T00:00:00.000000",
+ "2022-07-01T00:00:00.000000",
+ "2022-08-01T00:00:00.000000",
+ "2022-09-01T00:00:00.000000",
+ "2022-10-01T00:00:00.000000",
+ "2022-11-01T00:00:00.000000",
+ "2022-12-01T00:00:00.000000",
+ "2023-01-01T00:00:00.000000",
+ "2023-02-01T00:00:00.000000",
+ "2023-03-01T00:00:00.000000",
+ "2023-04-01T00:00:00.000000",
+ "2023-05-01T00:00:00.000000",
+ "2023-06-01T00:00:00.000000",
+ "2023-07-01T00:00:00.000000",
+ "2023-08-01T00:00:00.000000",
+ "2023-09-01T00:00:00.000000",
+ "2023-10-01T00:00:00.000000",
+ "2023-11-01T00:00:00.000000",
+ "2023-12-01T00:00:00.000000",
+ "2024-01-01T00:00:00.000000",
+ "2024-02-01T00:00:00.000000",
+ "2024-03-01T00:00:00.000000",
+ "2024-04-01T00:00:00.000000",
+ "2024-05-01T00:00:00.000000",
+ "2024-06-01T00:00:00.000000",
+ "2024-07-01T00:00:00.000000",
+ "2024-08-01T00:00:00.000000",
+ "2024-09-01T00:00:00.000000",
+ "2024-10-01T00:00:00.000000",
+ "2024-11-01T00:00:00.000000",
+ "2024-12-01T00:00:00.000000",
+ "2025-01-01T00:00:00.000000",
+ "2025-02-01T00:00:00.000000",
+ "2025-03-01T00:00:00.000000",
+ "2025-04-01T00:00:00.000000",
+ "2025-05-01T00:00:00.000000",
+ "2025-06-01T00:00:00.000000",
+ "2025-07-01T00:00:00.000000",
+ "2025-08-01T00:00:00.000000",
+ "2025-09-01T00:00:00.000000",
+ "2025-10-01T00:00:00.000000",
+ "2025-11-01T00:00:00.000000",
+ "2025-12-01T00:00:00.000000",
+ "2026-01-01T00:00:00.000000",
+ "2026-02-01T00:00:00.000000",
+ "2026-03-01T00:00:00.000000",
+ "2026-04-01T00:00:00.000000",
+ "2026-05-01T00:00:00.000000",
+ "2026-06-01T00:00:00.000000"
+ ],
+ "y": {
+ "bdata": "EYzXEh5NikAPVSPsL3WMQNyt3eOYQ41AD7wPf/JGjkBCRWiKaB6PQFBYJHQsMZBATD6o+FZCkUCArcKAs8eMQIcQ7nPKkY9A9PflWB/XkEDEVMfH6UeRQPdCUAGxWJNAFOUYMZD+k0BoU/AJ292SQIAsPIBhNpRAvLKmc0fIlkAWzBKW3dKZQB7B2CNMNJlA2S0qb+MEmUAJEGQNvaiUQKaNIhIhN4xAiaMccJjyjEBl2lM56dOIQF3rgxlp5YhA2kakjEddi0CBxTgl27iLQJNFz55LOoxAEgN5/0kxjEAUd/KEPnGMQHyVeTp6No1ApB/dRQcVikBgNhmuyQiIQBo7Je7QZ4pA2kacit28iUCyR8l+srWIQB3GKsbZ+4dAkRIcSOffiEBm5hZxwHqIQGBJ31XqeolA5cFWU7bIiUCoCg/0WRiMQEgb/h77BoxAIJFkKVTRiUCkRByi1TGKQGmvT2S+5IpAo5zOKp/Bi0Cssz2m23SNQFWwDB81KJBAE3QD9X2zkUApLm2zG5+RQPYmAwYnGJBAzadrmEytkEA4IWXXloSQQCyUWMprqY5A5K/SKk42jEBdmkv6BjSNQDCNpCJTGI1AgpeTNg0IkECc4lHgmSuQQAcQmh8gOZBAJjCv7nOIjkDhFf4fzKmOQIruZAy8Yo9AmMzc1RInkEA3bqoqtoSRQB/FErmkxZFAVMJHZhuokUCzwChxuVKRQA==",
+ "dtype": "f8"
+ }
+ }
+ ],
+ "layout": {
+ "annotations": [
+ {
+ "font": {
+ "size": 9
+ },
+ "showarrow": false,
+ "text": "2021-05
event",
+ "x": "2021-05-01T00:00:00",
+ "xanchor": "left",
+ "xref": "x",
+ "y": 1,
+ "yanchor": "top",
+ "yref": "y domain"
+ },
+ {
+ "font": {
+ "size": 9
+ },
+ "showarrow": false,
+ "text": "2022-01
event",
+ "x": "2022-01-01T00:00:00",
+ "xanchor": "left",
+ "xref": "x",
+ "y": 1,
+ "yanchor": "top",
+ "yref": "y domain"
+ },
+ {
+ "font": {
+ "size": 9
+ },
+ "showarrow": false,
+ "text": "2023-04
event",
+ "x": "2023-04-01T00:00:00",
+ "xanchor": "left",
+ "xref": "x",
+ "y": 1,
+ "yanchor": "top",
+ "yref": "y domain"
+ },
+ {
+ "font": {
+ "size": 9
+ },
+ "showarrow": false,
+ "text": "2024-09
event",
+ "x": "2024-09-01T00:00:00",
+ "xanchor": "left",
+ "xref": "x",
+ "y": 1,
+ "yanchor": "top",
+ "yref": "y domain"
+ },
+ {
+ "font": {
+ "size": 9
+ },
+ "showarrow": false,
+ "text": "2023-07
quiet",
+ "x": "2023-07-01T00:00:00",
+ "xanchor": "left",
+ "xref": "x",
+ "y": 1,
+ "yanchor": "top",
+ "yref": "y domain"
+ },
+ {
+ "font": {
+ "size": 9
+ },
+ "showarrow": false,
+ "text": "2024-11
quiet",
+ "x": "2024-11-01T00:00:00",
+ "xanchor": "left",
+ "xref": "x",
+ "y": 1,
+ "yanchor": "top",
+ "yref": "y domain"
+ },
+ {
+ "font": {
+ "size": 9
+ },
+ "showarrow": false,
+ "text": "2025-08
quiet",
+ "x": "2025-08-01T00:00:00",
+ "xanchor": "left",
+ "xref": "x",
+ "y": 1,
+ "yanchor": "top",
+ "yref": "y domain"
+ }
+ ],
+ "font": {
+ "color": "#52514e",
+ "size": 12
+ },
+ "height": 520,
+ "hovermode": "x unified",
+ "legend": {
+ "orientation": "h",
+ "x": 0,
+ "xanchor": "left",
+ "y": 1.02,
+ "yanchor": "bottom"
+ },
+ "margin": {
+ "b": 55,
+ "l": 70,
+ "r": 110,
+ "t": 70
+ },
+ "paper_bgcolor": "#fcfcfb",
+ "plot_bgcolor": "#fcfcfb",
+ "shapes": [
+ {
+ "fillcolor": "rgba(235, 104, 52, 0.13)",
+ "layer": "below",
+ "line": {
+ "width": 0
+ },
+ "type": "rect",
+ "x0": "2021-05-01T00:00:00",
+ "x1": "2021-11-01T00:00:00",
+ "xref": "x",
+ "y0": 0,
+ "y1": 1,
+ "yref": "y domain"
+ },
+ {
+ "fillcolor": "rgba(235, 104, 52, 0.13)",
+ "layer": "below",
+ "line": {
+ "width": 0
+ },
+ "type": "rect",
+ "x0": "2022-01-01T00:00:00",
+ "x1": "2022-07-01T00:00:00",
+ "xref": "x",
+ "y0": 0,
+ "y1": 1,
+ "yref": "y domain"
+ },
+ {
+ "fillcolor": "rgba(235, 104, 52, 0.13)",
+ "layer": "below",
+ "line": {
+ "width": 0
+ },
+ "type": "rect",
+ "x0": "2023-04-01T00:00:00",
+ "x1": "2023-10-01T00:00:00",
+ "xref": "x",
+ "y0": 0,
+ "y1": 1,
+ "yref": "y domain"
+ },
+ {
+ "fillcolor": "rgba(235, 104, 52, 0.13)",
+ "layer": "below",
+ "line": {
+ "width": 0
+ },
+ "type": "rect",
+ "x0": "2024-09-01T00:00:00",
+ "x1": "2025-03-01T00:00:00",
+ "xref": "x",
+ "y0": 0,
+ "y1": 1,
+ "yref": "y domain"
+ },
+ {
+ "fillcolor": "rgba(42, 120, 214, 0.10)",
+ "layer": "below",
+ "line": {
+ "width": 0
+ },
+ "type": "rect",
+ "x0": "2023-07-01T00:00:00",
+ "x1": "2024-01-01T00:00:00",
+ "xref": "x",
+ "y0": 0,
+ "y1": 1,
+ "yref": "y domain"
+ },
+ {
+ "fillcolor": "rgba(42, 120, 214, 0.10)",
+ "layer": "below",
+ "line": {
+ "width": 0
+ },
+ "type": "rect",
+ "x0": "2024-11-01T00:00:00",
+ "x1": "2025-05-01T00:00:00",
+ "xref": "x",
+ "y0": 0,
+ "y1": 1,
+ "yref": "y domain"
+ },
+ {
+ "fillcolor": "rgba(42, 120, 214, 0.10)",
+ "layer": "below",
+ "line": {
+ "width": 0
+ },
+ "type": "rect",
+ "x0": "2025-08-01T00:00:00",
+ "x1": "2026-02-01T00:00:00",
+ "xref": "x",
+ "y0": 0,
+ "y1": 1,
+ "yref": "y domain"
+ }
+ ],
+ "template": {
+ "data": {
+ "bar": [
+ {
+ "error_x": {
+ "color": "#2a3f5f"
+ },
+ "error_y": {
+ "color": "#2a3f5f"
+ },
+ "marker": {
+ "line": {
+ "color": "#E5ECF6",
+ "width": 0.5
+ },
+ "pattern": {
+ "fillmode": "overlay",
+ "size": 10,
+ "solidity": 0.2
+ }
+ },
+ "type": "bar"
+ }
+ ],
+ "barpolar": [
+ {
+ "marker": {
+ "line": {
+ "color": "#E5ECF6",
+ "width": 0.5
+ },
+ "pattern": {
+ "fillmode": "overlay",
+ "size": 10,
+ "solidity": 0.2
+ }
+ },
+ "type": "barpolar"
+ }
+ ],
+ "carpet": [
+ {
+ "aaxis": {
+ "endlinecolor": "#2a3f5f",
+ "gridcolor": "white",
+ "linecolor": "white",
+ "minorgridcolor": "white",
+ "startlinecolor": "#2a3f5f"
+ },
+ "baxis": {
+ "endlinecolor": "#2a3f5f",
+ "gridcolor": "white",
+ "linecolor": "white",
+ "minorgridcolor": "white",
+ "startlinecolor": "#2a3f5f"
+ },
+ "type": "carpet"
+ }
+ ],
+ "choropleth": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "type": "choropleth"
+ }
+ ],
+ "contour": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "colorscale": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "type": "contour"
+ }
+ ],
+ "contourcarpet": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "type": "contourcarpet"
+ }
+ ],
+ "heatmap": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "colorscale": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "type": "heatmap"
+ }
+ ],
+ "histogram": [
+ {
+ "marker": {
+ "pattern": {
+ "fillmode": "overlay",
+ "size": 10,
+ "solidity": 0.2
+ }
+ },
+ "type": "histogram"
+ }
+ ],
+ "histogram2d": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "colorscale": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "type": "histogram2d"
+ }
+ ],
+ "histogram2dcontour": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "colorscale": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "type": "histogram2dcontour"
+ }
+ ],
+ "mesh3d": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "type": "mesh3d"
+ }
+ ],
+ "parcoords": [
+ {
+ "line": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "parcoords"
+ }
+ ],
+ "pie": [
+ {
+ "automargin": true,
+ "type": "pie"
+ }
+ ],
+ "scatter": [
+ {
+ "fillpattern": {
+ "fillmode": "overlay",
+ "size": 10,
+ "solidity": 0.2
+ },
+ "type": "scatter"
+ }
+ ],
+ "scatter3d": [
+ {
+ "line": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scatter3d"
+ }
+ ],
+ "scattercarpet": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scattercarpet"
+ }
+ ],
+ "scattergeo": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scattergeo"
+ }
+ ],
+ "scattergl": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scattergl"
+ }
+ ],
+ "scattermap": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scattermap"
+ }
+ ],
+ "scattermapbox": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scattermapbox"
+ }
+ ],
+ "scatterpolar": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scatterpolar"
+ }
+ ],
+ "scatterpolargl": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scatterpolargl"
+ }
+ ],
+ "scatterternary": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scatterternary"
+ }
+ ],
+ "surface": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "colorscale": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "type": "surface"
+ }
+ ],
+ "table": [
+ {
+ "cells": {
+ "fill": {
+ "color": "#EBF0F8"
+ },
+ "line": {
+ "color": "white"
+ }
+ },
+ "header": {
+ "fill": {
+ "color": "#C8D4E3"
+ },
+ "line": {
+ "color": "white"
+ }
+ },
+ "type": "table"
+ }
+ ]
+ },
+ "layout": {
+ "annotationdefaults": {
+ "arrowcolor": "#2a3f5f",
+ "arrowhead": 0,
+ "arrowwidth": 1
+ },
+ "autotypenumbers": "strict",
+ "coloraxis": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "colorscale": {
+ "diverging": [
+ [
+ 0,
+ "#8e0152"
+ ],
+ [
+ 0.1,
+ "#c51b7d"
+ ],
+ [
+ 0.2,
+ "#de77ae"
+ ],
+ [
+ 0.3,
+ "#f1b6da"
+ ],
+ [
+ 0.4,
+ "#fde0ef"
+ ],
+ [
+ 0.5,
+ "#f7f7f7"
+ ],
+ [
+ 0.6,
+ "#e6f5d0"
+ ],
+ [
+ 0.7,
+ "#b8e186"
+ ],
+ [
+ 0.8,
+ "#7fbc41"
+ ],
+ [
+ 0.9,
+ "#4d9221"
+ ],
+ [
+ 1,
+ "#276419"
+ ]
+ ],
+ "sequential": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "sequentialminus": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ]
+ },
+ "colorway": [
+ "#636efa",
+ "#EF553B",
+ "#00cc96",
+ "#ab63fa",
+ "#FFA15A",
+ "#19d3f3",
+ "#FF6692",
+ "#B6E880",
+ "#FF97FF",
+ "#FECB52"
+ ],
+ "font": {
+ "color": "#2a3f5f"
+ },
+ "geo": {
+ "bgcolor": "white",
+ "lakecolor": "white",
+ "landcolor": "#E5ECF6",
+ "showlakes": true,
+ "showland": true,
+ "subunitcolor": "white"
+ },
+ "hoverlabel": {
+ "align": "left"
+ },
+ "hovermode": "closest",
+ "mapbox": {
+ "style": "light"
+ },
+ "paper_bgcolor": "white",
+ "plot_bgcolor": "#E5ECF6",
+ "polar": {
+ "angularaxis": {
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": ""
+ },
+ "bgcolor": "#E5ECF6",
+ "radialaxis": {
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": ""
+ }
+ },
+ "scene": {
+ "xaxis": {
+ "backgroundcolor": "#E5ECF6",
+ "gridcolor": "white",
+ "gridwidth": 2,
+ "linecolor": "white",
+ "showbackground": true,
+ "ticks": "",
+ "zerolinecolor": "white"
+ },
+ "yaxis": {
+ "backgroundcolor": "#E5ECF6",
+ "gridcolor": "white",
+ "gridwidth": 2,
+ "linecolor": "white",
+ "showbackground": true,
+ "ticks": "",
+ "zerolinecolor": "white"
+ },
+ "zaxis": {
+ "backgroundcolor": "#E5ECF6",
+ "gridcolor": "white",
+ "gridwidth": 2,
+ "linecolor": "white",
+ "showbackground": true,
+ "ticks": "",
+ "zerolinecolor": "white"
+ }
+ },
+ "shapedefaults": {
+ "line": {
+ "color": "#2a3f5f"
+ }
+ },
+ "ternary": {
+ "aaxis": {
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": ""
+ },
+ "baxis": {
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": ""
+ },
+ "bgcolor": "#E5ECF6",
+ "caxis": {
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": ""
+ }
+ },
+ "title": {
+ "x": 0.05
+ },
+ "xaxis": {
+ "automargin": true,
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": "",
+ "title": {
+ "standoff": 15
+ },
+ "zerolinecolor": "white",
+ "zerolinewidth": 2
+ },
+ "yaxis": {
+ "automargin": true,
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": "",
+ "title": {
+ "standoff": 15
+ },
+ "zerolinecolor": "white",
+ "zerolinewidth": 2
+ }
+ }
+ },
+ "title": {
+ "font": {
+ "color": "#0b0b0b",
+ "size": 17
+ },
+ "text": "Candidate cutoffs and their 6-month forecast windows"
+ },
+ "xaxis": {
+ "gridcolor": "#e6e5e1",
+ "linecolor": "#e6e5e1",
+ "showspikes": false,
+ "zeroline": false
+ },
+ "yaxis": {
+ "gridcolor": "#e6e5e1",
+ "linecolor": "#e6e5e1",
+ "showspikes": false,
+ "title": {
+ "text": "USD / metric ton"
+ },
+ "zeroline": false
+ }
+ }
+ }
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "plot_cutoff_windows(prices)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "b88359ce",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-08-06T19:18:04.574898Z",
+ "iopub.status.busy": "2026-08-06T19:18:04.574607Z",
+ "iopub.status.idle": "2026-08-06T19:18:04.587116Z",
+ "shell.execute_reply": "2026-08-06T19:18:04.585524Z"
+ }
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " | \n",
+ " cutoff | \n",
+ " kind | \n",
+ " reason | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " | 0 | \n",
+ " 2021-05 | \n",
+ " event | \n",
+ " June 2021 crash, -16.6% | \n",
+ "
\n",
+ " \n",
+ " | 1 | \n",
+ " 2022-01 | \n",
+ " event | \n",
+ " Indonesia export ban, -29.4% over 6mo | \n",
+ "
\n",
+ " \n",
+ " | 2 | \n",
+ " 2023-04 | \n",
+ " event | \n",
+ " May 2023 correction, -10.7% | \n",
+ "
\n",
+ " \n",
+ " | 3 | \n",
+ " 2024-09 | \n",
+ " event | \n",
+ " Oct 2024 rally, +9.7% | \n",
+ "
\n",
+ " \n",
+ " | 4 | \n",
+ " 2023-07 | \n",
+ " quiet | \n",
+ " calmest window, max move 4.1% | \n",
+ "
\n",
+ " \n",
+ " | 5 | \n",
+ " 2024-11 | \n",
+ " quiet | \n",
+ " max move 8.7% | \n",
+ "
\n",
+ " \n",
+ " | 6 | \n",
+ " 2025-08 | \n",
+ " quiet | \n",
+ " max move 5.9% | \n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ " cutoff kind reason\n",
+ "0 2021-05 event June 2021 crash, -16.6%\n",
+ "1 2022-01 event Indonesia export ban, -29.4% over 6mo\n",
+ "2 2023-04 event May 2023 correction, -10.7%\n",
+ "3 2024-09 event Oct 2024 rally, +9.7%\n",
+ "4 2023-07 quiet calmest window, max move 4.1%\n",
+ "5 2024-11 quiet max move 8.7%\n",
+ "6 2025-08 quiet max move 5.9%"
+ ]
+ },
+ "execution_count": 5,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# The cutoffs and why each was picked — edit this list to try alternatives.\n",
+ "pd.DataFrame([{\"cutoff\": c.date[:7], \"kind\": c.kind, \"reason\": c.label} for c in DEFAULT_CUTOFFS])"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "9e87c51d",
+ "metadata": {},
+ "source": [
+ "---\n",
+ "## 5. What the model can actually see\n",
+ "\n",
+ "This is the chart that makes the publication lag concrete.\n",
+ "\n",
+ "The dotted grey line is what really happened. Each coloured line is the history that\n",
+ "was **published** as of one cutoff — where it stops is the newest price a forecaster\n",
+ "had on that date.\n",
+ "\n",
+ "The horizontal distance between where a coloured line ends and its cutoff is the\n",
+ "information gap. Normally 2 months; far worse inside a blackout.\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "bc67c86c",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-08-06T19:18:04.590144Z",
+ "iopub.status.busy": "2026-08-06T19:18:04.589809Z",
+ "iopub.status.idle": "2026-08-06T19:18:04.648707Z",
+ "shell.execute_reply": "2026-08-06T19:18:04.646945Z"
+ }
+ },
+ "outputs": [
+ {
+ "data": {
+ "application/vnd.plotly.v1+json": {
+ "config": {
+ "plotlyServerURL": "https://plot.ly"
+ },
+ "data": [
+ {
+ "hovertemplate": "%{x|%b %Y}
actual $%{y:.0f}",
+ "line": {
+ "color": "#52514e",
+ "dash": "dot",
+ "width": 1.5
+ },
+ "mode": "lines",
+ "name": "What actually happened",
+ "type": "scatter",
+ "x": [
+ "2020-05-01T00:00:00.000000",
+ "2020-06-01T00:00:00.000000",
+ "2020-07-01T00:00:00.000000",
+ "2020-08-01T00:00:00.000000",
+ "2020-09-01T00:00:00.000000",
+ "2020-10-01T00:00:00.000000",
+ "2020-11-01T00:00:00.000000",
+ "2020-12-01T00:00:00.000000",
+ "2021-01-01T00:00:00.000000",
+ "2021-02-01T00:00:00.000000",
+ "2021-03-01T00:00:00.000000",
+ "2021-04-01T00:00:00.000000",
+ "2021-05-01T00:00:00.000000",
+ "2021-06-01T00:00:00.000000",
+ "2021-07-01T00:00:00.000000",
+ "2021-08-01T00:00:00.000000",
+ "2021-09-01T00:00:00.000000",
+ "2021-10-01T00:00:00.000000",
+ "2021-11-01T00:00:00.000000",
+ "2021-12-01T00:00:00.000000",
+ "2022-01-01T00:00:00.000000",
+ "2022-02-01T00:00:00.000000",
+ "2022-03-01T00:00:00.000000",
+ "2022-04-01T00:00:00.000000",
+ "2022-05-01T00:00:00.000000",
+ "2022-06-01T00:00:00.000000",
+ "2022-07-01T00:00:00.000000",
+ "2022-08-01T00:00:00.000000",
+ "2022-09-01T00:00:00.000000",
+ "2022-10-01T00:00:00.000000",
+ "2022-11-01T00:00:00.000000",
+ "2022-12-01T00:00:00.000000",
+ "2023-01-01T00:00:00.000000",
+ "2023-02-01T00:00:00.000000",
+ "2023-03-01T00:00:00.000000",
+ "2023-04-01T00:00:00.000000",
+ "2023-05-01T00:00:00.000000",
+ "2023-06-01T00:00:00.000000",
+ "2023-07-01T00:00:00.000000",
+ "2023-08-01T00:00:00.000000",
+ "2023-09-01T00:00:00.000000",
+ "2023-10-01T00:00:00.000000",
+ "2023-11-01T00:00:00.000000",
+ "2023-12-01T00:00:00.000000",
+ "2024-01-01T00:00:00.000000",
+ "2024-02-01T00:00:00.000000",
+ "2024-03-01T00:00:00.000000",
+ "2024-04-01T00:00:00.000000",
+ "2024-05-01T00:00:00.000000",
+ "2024-06-01T00:00:00.000000",
+ "2024-07-01T00:00:00.000000",
+ "2024-08-01T00:00:00.000000",
+ "2024-09-01T00:00:00.000000",
+ "2024-10-01T00:00:00.000000",
+ "2024-11-01T00:00:00.000000",
+ "2024-12-01T00:00:00.000000",
+ "2025-01-01T00:00:00.000000",
+ "2025-02-01T00:00:00.000000",
+ "2025-03-01T00:00:00.000000",
+ "2025-04-01T00:00:00.000000",
+ "2025-05-01T00:00:00.000000",
+ "2025-06-01T00:00:00.000000",
+ "2025-07-01T00:00:00.000000",
+ "2025-08-01T00:00:00.000000",
+ "2025-09-01T00:00:00.000000",
+ "2025-10-01T00:00:00.000000",
+ "2025-11-01T00:00:00.000000",
+ "2025-12-01T00:00:00.000000",
+ "2026-01-01T00:00:00.000000",
+ "2026-02-01T00:00:00.000000",
+ "2026-03-01T00:00:00.000000",
+ "2026-04-01T00:00:00.000000",
+ "2026-05-01T00:00:00.000000"
+ ],
+ "y": {
+ "bdata": "13tn8rYnf0ARMrznyemBQNoB1/v2E4NAAl1nc5gVhUC3Kw8tmhqGQKHUtTyZ14ZAEYzXEh5NikAPVSPsL3WMQNyt3eOYQ41AD7wPf/JGjkBCRWiKaB6PQFBYJHQsMZBATD6o+FZCkUCArcKAs8eMQIcQ7nPKkY9A9PflWB/XkEDEVMfH6UeRQPdCUAGxWJNAFOUYMZD+k0BoU/AJ292SQIAsPIBhNpRAvLKmc0fIlkAWzBKW3dKZQB7B2CNMNJlA2S0qb+MEmUAJEGQNvaiUQKaNIhIhN4xAiaMccJjyjEBl2lM56dOIQF3rgxlp5YhA2kakjEddi0CBxTgl27iLQJNFz55LOoxAEgN5/0kxjEAUd/KEPnGMQHyVeTp6No1ApB/dRQcVikBgNhmuyQiIQBo7Je7QZ4pA2kacit28iUCyR8l+srWIQB3GKsbZ+4dAkRIcSOffiEBm5hZxwHqIQGBJ31XqeolA5cFWU7bIiUCoCg/0WRiMQEgb/h77BoxAIJFkKVTRiUCkRByi1TGKQGmvT2S+5IpAo5zOKp/Bi0Cssz2m23SNQFWwDB81KJBAE3QD9X2zkUApLm2zG5+RQPYmAwYnGJBAzadrmEytkEA4IWXXloSQQCyUWMprqY5A5K/SKk42jEBdmkv6BjSNQDCNpCJTGI1AgpeTNg0IkECc4lHgmSuQQAcQmh8gOZBAJjCv7nOIjkDhFf4fzKmOQIruZAy8Yo9AmMzc1RInkEA3bqoqtoSRQB/FErmkxZFAVMJHZhuokUA=",
+ "dtype": "f8"
+ }
+ },
+ {
+ "hovertemplate": "as of 2021-05
%{x|%b %Y} $%{y:.0f}",
+ "line": {
+ "color": "#2a78d6",
+ "width": 2
+ },
+ "mode": "lines",
+ "name": "2021-05 (event)",
+ "type": "scatter",
+ "x": [
+ "2020-05-01T00:00:00.000000",
+ "2020-06-01T00:00:00.000000",
+ "2020-07-01T00:00:00.000000",
+ "2020-08-01T00:00:00.000000",
+ "2020-09-01T00:00:00.000000",
+ "2020-10-01T00:00:00.000000",
+ "2020-11-01T00:00:00.000000",
+ "2020-12-01T00:00:00.000000",
+ "2021-01-01T00:00:00.000000",
+ "2021-02-01T00:00:00.000000",
+ "2021-03-01T00:00:00.000000"
+ ],
+ "y": {
+ "bdata": "13tn8rYnf0ARMrznyemBQNoB1/v2E4NAAl1nc5gVhUC3Kw8tmhqGQKHUtTyZ14ZAEYzXEh5NikAPVSPsL3WMQNyt3eOYQ41AD7wPf/JGjkBCRWiKaB6PQA==",
+ "dtype": "f8"
+ }
+ },
+ {
+ "hovertemplate": "newest price available at 2021-05
$%{y:.0f}",
+ "marker": {
+ "color": "#2a78d6",
+ "line": {
+ "color": "#fcfcfb",
+ "width": 2
+ },
+ "size": 9
+ },
+ "mode": "markers+text",
+ "showlegend": false,
+ "text": [
+ " 2021-05"
+ ],
+ "textfont": {
+ "color": "#0b0b0b",
+ "size": 10
+ },
+ "textposition": "middle right",
+ "type": "scatter",
+ "x": [
+ "2021-03-01T00:00:00"
+ ],
+ "y": [
+ 995.8010452409874
+ ]
+ },
+ {
+ "hovertemplate": "as of 2022-01
%{x|%b %Y} $%{y:.0f}",
+ "line": {
+ "color": "#eb6834",
+ "width": 2
+ },
+ "mode": "lines",
+ "name": "2022-01 (event)",
+ "type": "scatter",
+ "x": [
+ "2020-05-01T00:00:00.000000",
+ "2020-06-01T00:00:00.000000",
+ "2020-07-01T00:00:00.000000",
+ "2020-08-01T00:00:00.000000",
+ "2020-09-01T00:00:00.000000",
+ "2020-10-01T00:00:00.000000",
+ "2020-11-01T00:00:00.000000",
+ "2020-12-01T00:00:00.000000",
+ "2021-01-01T00:00:00.000000",
+ "2021-02-01T00:00:00.000000",
+ "2021-03-01T00:00:00.000000",
+ "2021-04-01T00:00:00.000000",
+ "2021-05-01T00:00:00.000000",
+ "2021-06-01T00:00:00.000000",
+ "2021-07-01T00:00:00.000000",
+ "2021-08-01T00:00:00.000000",
+ "2021-09-01T00:00:00.000000",
+ "2021-10-01T00:00:00.000000",
+ "2021-11-01T00:00:00.000000"
+ ],
+ "y": {
+ "bdata": "13tn8rYnf0ARMrznyemBQNoB1/v2E4NAAl1nc5gVhUC3Kw8tmhqGQKHUtTyZ14ZAEYzXEh5NikAPVSPsL3WMQNyt3eOYQ41AD7wPf/JGjkBCRWiKaB6PQFBYJHQsMZBATD6o+FZCkUCArcKAs8eMQIcQ7nPKkY9A9PflWB/XkEDEVMfH6UeRQPdCUAGxWJNAFOUYMZD+k0A=",
+ "dtype": "f8"
+ }
+ },
+ {
+ "hovertemplate": "newest price available at 2022-01
$%{y:.0f}",
+ "marker": {
+ "color": "#eb6834",
+ "line": {
+ "color": "#fcfcfb",
+ "width": 2
+ },
+ "size": 9
+ },
+ "mode": "markers+text",
+ "showlegend": false,
+ "text": [
+ " 2022-01"
+ ],
+ "textfont": {
+ "color": "#0b0b0b",
+ "size": 10
+ },
+ "textposition": "middle right",
+ "type": "scatter",
+ "x": [
+ "2021-11-01T00:00:00"
+ ],
+ "y": [
+ 1279.640812291128
+ ]
+ },
+ {
+ "hovertemplate": "as of 2023-04
%{x|%b %Y} $%{y:.0f}",
+ "line": {
+ "color": "#1baf7a",
+ "width": 2
+ },
+ "mode": "lines",
+ "name": "2023-04 (event)",
+ "type": "scatter",
+ "x": [
+ "2020-05-01T00:00:00.000000",
+ "2020-06-01T00:00:00.000000",
+ "2020-07-01T00:00:00.000000",
+ "2020-08-01T00:00:00.000000",
+ "2020-09-01T00:00:00.000000",
+ "2020-10-01T00:00:00.000000",
+ "2020-11-01T00:00:00.000000",
+ "2020-12-01T00:00:00.000000",
+ "2021-01-01T00:00:00.000000",
+ "2021-02-01T00:00:00.000000",
+ "2021-03-01T00:00:00.000000",
+ "2021-04-01T00:00:00.000000",
+ "2021-05-01T00:00:00.000000",
+ "2021-06-01T00:00:00.000000",
+ "2021-07-01T00:00:00.000000",
+ "2021-08-01T00:00:00.000000",
+ "2021-09-01T00:00:00.000000",
+ "2021-10-01T00:00:00.000000",
+ "2021-11-01T00:00:00.000000",
+ "2021-12-01T00:00:00.000000",
+ "2022-01-01T00:00:00.000000",
+ "2022-02-01T00:00:00.000000",
+ "2022-03-01T00:00:00.000000",
+ "2022-04-01T00:00:00.000000",
+ "2022-05-01T00:00:00.000000",
+ "2022-06-01T00:00:00.000000",
+ "2022-07-01T00:00:00.000000",
+ "2022-08-01T00:00:00.000000",
+ "2022-09-01T00:00:00.000000",
+ "2022-10-01T00:00:00.000000",
+ "2022-11-01T00:00:00.000000",
+ "2022-12-01T00:00:00.000000",
+ "2023-01-01T00:00:00.000000",
+ "2023-02-01T00:00:00.000000"
+ ],
+ "y": {
+ "bdata": "13tn8rYnf0ARMrznyemBQNoB1/v2E4NAAl1nc5gVhUC3Kw8tmhqGQKHUtTyZ14ZAEYzXEh5NikAPVSPsL3WMQNyt3eOYQ41AD7wPf/JGjkBCRWiKaB6PQFBYJHQsMZBATD6o+FZCkUCArcKAs8eMQIcQ7nPKkY9A9PflWB/XkEDEVMfH6UeRQPdCUAGxWJNAFOUYMZD+k0BoU/AJ292SQIAsPIBhNpRAvLKmc0fIlkAWzBKW3dKZQB7B2CNMNJlA2S0qb+MEmUAJEGQNvaiUQKaNIhIhN4xAiaMccJjyjEBl2lM56dOIQF3rgxlp5YhA2kakjEddi0CBxTgl27iLQJNFz55LOoxAEgN5/0kxjEA=",
+ "dtype": "f8"
+ }
+ },
+ {
+ "hovertemplate": "newest price available at 2023-04
$%{y:.0f}",
+ "marker": {
+ "color": "#1baf7a",
+ "line": {
+ "color": "#fcfcfb",
+ "width": 2
+ },
+ "size": 9
+ },
+ "mode": "markers+text",
+ "showlegend": false,
+ "text": [
+ " 2023-04"
+ ],
+ "textfont": {
+ "color": "#0b0b0b",
+ "size": 10
+ },
+ "textposition": "middle right",
+ "type": "scatter",
+ "x": [
+ "2023-02-01T00:00:00"
+ ],
+ "y": [
+ 902.161131806761
+ ]
+ },
+ {
+ "hovertemplate": "as of 2024-09
%{x|%b %Y} $%{y:.0f}",
+ "line": {
+ "color": "#eda100",
+ "width": 2
+ },
+ "mode": "lines",
+ "name": "2024-09 (event)",
+ "type": "scatter",
+ "x": [
+ "2020-05-01T00:00:00.000000",
+ "2020-06-01T00:00:00.000000",
+ "2020-07-01T00:00:00.000000",
+ "2020-08-01T00:00:00.000000",
+ "2020-09-01T00:00:00.000000",
+ "2020-10-01T00:00:00.000000",
+ "2020-11-01T00:00:00.000000",
+ "2020-12-01T00:00:00.000000",
+ "2021-01-01T00:00:00.000000",
+ "2021-02-01T00:00:00.000000",
+ "2021-03-01T00:00:00.000000",
+ "2021-04-01T00:00:00.000000",
+ "2021-05-01T00:00:00.000000",
+ "2021-06-01T00:00:00.000000",
+ "2021-07-01T00:00:00.000000",
+ "2021-08-01T00:00:00.000000",
+ "2021-09-01T00:00:00.000000",
+ "2021-10-01T00:00:00.000000",
+ "2021-11-01T00:00:00.000000",
+ "2021-12-01T00:00:00.000000",
+ "2022-01-01T00:00:00.000000",
+ "2022-02-01T00:00:00.000000",
+ "2022-03-01T00:00:00.000000",
+ "2022-04-01T00:00:00.000000",
+ "2022-05-01T00:00:00.000000",
+ "2022-06-01T00:00:00.000000",
+ "2022-07-01T00:00:00.000000",
+ "2022-08-01T00:00:00.000000",
+ "2022-09-01T00:00:00.000000",
+ "2022-10-01T00:00:00.000000",
+ "2022-11-01T00:00:00.000000",
+ "2022-12-01T00:00:00.000000",
+ "2023-01-01T00:00:00.000000",
+ "2023-02-01T00:00:00.000000",
+ "2023-03-01T00:00:00.000000",
+ "2023-04-01T00:00:00.000000",
+ "2023-05-01T00:00:00.000000",
+ "2023-06-01T00:00:00.000000",
+ "2023-07-01T00:00:00.000000",
+ "2023-08-01T00:00:00.000000",
+ "2023-09-01T00:00:00.000000",
+ "2023-10-01T00:00:00.000000",
+ "2023-11-01T00:00:00.000000",
+ "2023-12-01T00:00:00.000000",
+ "2024-01-01T00:00:00.000000",
+ "2024-02-01T00:00:00.000000",
+ "2024-03-01T00:00:00.000000",
+ "2024-04-01T00:00:00.000000",
+ "2024-05-01T00:00:00.000000",
+ "2024-06-01T00:00:00.000000",
+ "2024-07-01T00:00:00.000000"
+ ],
+ "y": {
+ "bdata": "13tn8rYnf0ARMrznyemBQNoB1/v2E4NAAl1nc5gVhUC3Kw8tmhqGQKHUtTyZ14ZAEYzXEh5NikAPVSPsL3WMQNyt3eOYQ41AD7wPf/JGjkBCRWiKaB6PQFBYJHQsMZBATD6o+FZCkUCArcKAs8eMQIcQ7nPKkY9A9PflWB/XkEDEVMfH6UeRQPdCUAGxWJNAFOUYMZD+k0BoU/AJ292SQIAsPIBhNpRAvLKmc0fIlkAWzBKW3dKZQB7B2CNMNJlA2S0qb+MEmUAJEGQNvaiUQKaNIhIhN4xAiaMccJjyjEBl2lM56dOIQF3rgxlp5YhA2kakjEddi0CBxTgl27iLQJNFz55LOoxAEgN5/0kxjEAUd/KEPnGMQHyVeTp6No1ApB/dRQcVikBgNhmuyQiIQBo7Je7QZ4pA2kacit28iUCyR8l+srWIQB3GKsbZ+4dAkRIcSOffiEBm5hZxwHqIQGBJ31XqeolA5cFWU7bIiUCoCg/0WRiMQEgb/h77BoxAIJFkKVTRiUCkRByi1TGKQGmvT2S+5IpA",
+ "dtype": "f8"
+ }
+ },
+ {
+ "hovertemplate": "newest price available at 2024-09
$%{y:.0f}",
+ "marker": {
+ "color": "#eda100",
+ "line": {
+ "color": "#fcfcfb",
+ "width": 2
+ },
+ "size": 9
+ },
+ "mode": "markers+text",
+ "showlegend": false,
+ "text": [
+ " 2024-09"
+ ],
+ "textfont": {
+ "color": "#0b0b0b",
+ "size": 10
+ },
+ "textposition": "middle right",
+ "type": "scatter",
+ "x": [
+ "2024-07-01T00:00:00"
+ ],
+ "y": [
+ 860.5929647660643
+ ]
+ },
+ {
+ "hovertemplate": "as of 2023-07
%{x|%b %Y} $%{y:.0f}",
+ "line": {
+ "color": "#2a78d6",
+ "width": 2
+ },
+ "mode": "lines",
+ "name": "2023-07 (quiet)",
+ "type": "scatter",
+ "x": [
+ "2020-05-01T00:00:00.000000",
+ "2020-06-01T00:00:00.000000",
+ "2020-07-01T00:00:00.000000",
+ "2020-08-01T00:00:00.000000",
+ "2020-09-01T00:00:00.000000",
+ "2020-10-01T00:00:00.000000",
+ "2020-11-01T00:00:00.000000",
+ "2020-12-01T00:00:00.000000",
+ "2021-01-01T00:00:00.000000",
+ "2021-02-01T00:00:00.000000",
+ "2021-03-01T00:00:00.000000",
+ "2021-04-01T00:00:00.000000",
+ "2021-05-01T00:00:00.000000",
+ "2021-06-01T00:00:00.000000",
+ "2021-07-01T00:00:00.000000",
+ "2021-08-01T00:00:00.000000",
+ "2021-09-01T00:00:00.000000",
+ "2021-10-01T00:00:00.000000",
+ "2021-11-01T00:00:00.000000",
+ "2021-12-01T00:00:00.000000",
+ "2022-01-01T00:00:00.000000",
+ "2022-02-01T00:00:00.000000",
+ "2022-03-01T00:00:00.000000",
+ "2022-04-01T00:00:00.000000",
+ "2022-05-01T00:00:00.000000",
+ "2022-06-01T00:00:00.000000",
+ "2022-07-01T00:00:00.000000",
+ "2022-08-01T00:00:00.000000",
+ "2022-09-01T00:00:00.000000",
+ "2022-10-01T00:00:00.000000",
+ "2022-11-01T00:00:00.000000",
+ "2022-12-01T00:00:00.000000",
+ "2023-01-01T00:00:00.000000",
+ "2023-02-01T00:00:00.000000",
+ "2023-03-01T00:00:00.000000",
+ "2023-04-01T00:00:00.000000",
+ "2023-05-01T00:00:00.000000"
+ ],
+ "y": {
+ "bdata": "13tn8rYnf0ARMrznyemBQNoB1/v2E4NAAl1nc5gVhUC3Kw8tmhqGQKHUtTyZ14ZAEYzXEh5NikAPVSPsL3WMQNyt3eOYQ41AD7wPf/JGjkBCRWiKaB6PQFBYJHQsMZBATD6o+FZCkUCArcKAs8eMQIcQ7nPKkY9A9PflWB/XkEDEVMfH6UeRQPdCUAGxWJNAFOUYMZD+k0BoU/AJ292SQIAsPIBhNpRAvLKmc0fIlkAWzBKW3dKZQB7B2CNMNJlA2S0qb+MEmUAJEGQNvaiUQKaNIhIhN4xAiaMccJjyjEBl2lM56dOIQF3rgxlp5YhA2kakjEddi0CBxTgl27iLQJNFz55LOoxAEgN5/0kxjEAUd/KEPnGMQHyVeTp6No1ApB/dRQcVikA=",
+ "dtype": "f8"
+ }
+ },
+ {
+ "hovertemplate": "newest price available at 2023-07
$%{y:.0f}",
+ "marker": {
+ "color": "#2a78d6",
+ "line": {
+ "color": "#fcfcfb",
+ "width": 2
+ },
+ "size": 9
+ },
+ "mode": "markers+text",
+ "showlegend": false,
+ "text": [
+ " 2023-07"
+ ],
+ "textfont": {
+ "color": "#0b0b0b",
+ "size": 10
+ },
+ "textposition": "middle right",
+ "type": "scatter",
+ "x": [
+ "2023-05-01T00:00:00"
+ ],
+ "y": [
+ 834.6285512233048
+ ]
+ },
+ {
+ "hovertemplate": "as of 2024-11
%{x|%b %Y} $%{y:.0f}",
+ "line": {
+ "color": "#eb6834",
+ "width": 2
+ },
+ "mode": "lines",
+ "name": "2024-11 (quiet)",
+ "type": "scatter",
+ "x": [
+ "2020-05-01T00:00:00.000000",
+ "2020-06-01T00:00:00.000000",
+ "2020-07-01T00:00:00.000000",
+ "2020-08-01T00:00:00.000000",
+ "2020-09-01T00:00:00.000000",
+ "2020-10-01T00:00:00.000000",
+ "2020-11-01T00:00:00.000000",
+ "2020-12-01T00:00:00.000000",
+ "2021-01-01T00:00:00.000000",
+ "2021-02-01T00:00:00.000000",
+ "2021-03-01T00:00:00.000000",
+ "2021-04-01T00:00:00.000000",
+ "2021-05-01T00:00:00.000000",
+ "2021-06-01T00:00:00.000000",
+ "2021-07-01T00:00:00.000000",
+ "2021-08-01T00:00:00.000000",
+ "2021-09-01T00:00:00.000000",
+ "2021-10-01T00:00:00.000000",
+ "2021-11-01T00:00:00.000000",
+ "2021-12-01T00:00:00.000000",
+ "2022-01-01T00:00:00.000000",
+ "2022-02-01T00:00:00.000000",
+ "2022-03-01T00:00:00.000000",
+ "2022-04-01T00:00:00.000000",
+ "2022-05-01T00:00:00.000000",
+ "2022-06-01T00:00:00.000000",
+ "2022-07-01T00:00:00.000000",
+ "2022-08-01T00:00:00.000000",
+ "2022-09-01T00:00:00.000000",
+ "2022-10-01T00:00:00.000000",
+ "2022-11-01T00:00:00.000000",
+ "2022-12-01T00:00:00.000000",
+ "2023-01-01T00:00:00.000000",
+ "2023-02-01T00:00:00.000000",
+ "2023-03-01T00:00:00.000000",
+ "2023-04-01T00:00:00.000000",
+ "2023-05-01T00:00:00.000000",
+ "2023-06-01T00:00:00.000000",
+ "2023-07-01T00:00:00.000000",
+ "2023-08-01T00:00:00.000000",
+ "2023-09-01T00:00:00.000000",
+ "2023-10-01T00:00:00.000000",
+ "2023-11-01T00:00:00.000000",
+ "2023-12-01T00:00:00.000000",
+ "2024-01-01T00:00:00.000000",
+ "2024-02-01T00:00:00.000000",
+ "2024-03-01T00:00:00.000000",
+ "2024-04-01T00:00:00.000000",
+ "2024-05-01T00:00:00.000000",
+ "2024-06-01T00:00:00.000000",
+ "2024-07-01T00:00:00.000000",
+ "2024-08-01T00:00:00.000000",
+ "2024-09-01T00:00:00.000000"
+ ],
+ "y": {
+ "bdata": "13tn8rYnf0ARMrznyemBQNoB1/v2E4NAAl1nc5gVhUC3Kw8tmhqGQKHUtTyZ14ZAEYzXEh5NikAPVSPsL3WMQNyt3eOYQ41AD7wPf/JGjkBCRWiKaB6PQFBYJHQsMZBATD6o+FZCkUCArcKAs8eMQIcQ7nPKkY9A9PflWB/XkEDEVMfH6UeRQPdCUAGxWJNAFOUYMZD+k0BoU/AJ292SQIAsPIBhNpRAvLKmc0fIlkAWzBKW3dKZQB7B2CNMNJlA2S0qb+MEmUAJEGQNvaiUQKaNIhIhN4xAiaMccJjyjEBl2lM56dOIQF3rgxlp5YhA2kakjEddi0CBxTgl27iLQJNFz55LOoxAEgN5/0kxjEAUd/KEPnGMQHyVeTp6No1ApB/dRQcVikBgNhmuyQiIQBo7Je7QZ4pA2kacit28iUCyR8l+srWIQB3GKsbZ+4dAkRIcSOffiEBm5hZxwHqIQGBJ31XqeolA5cFWU7bIiUCoCg/0WRiMQEgb/h77BoxAIJFkKVTRiUCkRByi1TGKQGmvT2S+5IpAo5zOKp/Bi0Cssz2m23SNQA==",
+ "dtype": "f8"
+ }
+ },
+ {
+ "hovertemplate": "newest price available at 2024-11
$%{y:.0f}",
+ "marker": {
+ "color": "#eb6834",
+ "line": {
+ "color": "#fcfcfb",
+ "width": 2
+ },
+ "size": 9
+ },
+ "mode": "markers+text",
+ "showlegend": false,
+ "text": [
+ " 2024-11"
+ ],
+ "textfont": {
+ "color": "#0b0b0b",
+ "size": 10
+ },
+ "textposition": "middle right",
+ "type": "scatter",
+ "x": [
+ "2024-09-01T00:00:00"
+ ],
+ "y": [
+ 942.6072506733376
+ ]
+ },
+ {
+ "hovertemplate": "as of 2025-08
%{x|%b %Y} $%{y:.0f}",
+ "line": {
+ "color": "#1baf7a",
+ "width": 2
+ },
+ "mode": "lines",
+ "name": "2025-08 (quiet)",
+ "type": "scatter",
+ "x": [
+ "2020-05-01T00:00:00.000000",
+ "2020-06-01T00:00:00.000000",
+ "2020-07-01T00:00:00.000000",
+ "2020-08-01T00:00:00.000000",
+ "2020-09-01T00:00:00.000000",
+ "2020-10-01T00:00:00.000000",
+ "2020-11-01T00:00:00.000000",
+ "2020-12-01T00:00:00.000000",
+ "2021-01-01T00:00:00.000000",
+ "2021-02-01T00:00:00.000000",
+ "2021-03-01T00:00:00.000000",
+ "2021-04-01T00:00:00.000000",
+ "2021-05-01T00:00:00.000000",
+ "2021-06-01T00:00:00.000000",
+ "2021-07-01T00:00:00.000000",
+ "2021-08-01T00:00:00.000000",
+ "2021-09-01T00:00:00.000000",
+ "2021-10-01T00:00:00.000000",
+ "2021-11-01T00:00:00.000000",
+ "2021-12-01T00:00:00.000000",
+ "2022-01-01T00:00:00.000000",
+ "2022-02-01T00:00:00.000000",
+ "2022-03-01T00:00:00.000000",
+ "2022-04-01T00:00:00.000000",
+ "2022-05-01T00:00:00.000000",
+ "2022-06-01T00:00:00.000000",
+ "2022-07-01T00:00:00.000000",
+ "2022-08-01T00:00:00.000000",
+ "2022-09-01T00:00:00.000000",
+ "2022-10-01T00:00:00.000000",
+ "2022-11-01T00:00:00.000000",
+ "2022-12-01T00:00:00.000000",
+ "2023-01-01T00:00:00.000000",
+ "2023-02-01T00:00:00.000000",
+ "2023-03-01T00:00:00.000000",
+ "2023-04-01T00:00:00.000000",
+ "2023-05-01T00:00:00.000000",
+ "2023-06-01T00:00:00.000000",
+ "2023-07-01T00:00:00.000000",
+ "2023-08-01T00:00:00.000000",
+ "2023-09-01T00:00:00.000000",
+ "2023-10-01T00:00:00.000000",
+ "2023-11-01T00:00:00.000000",
+ "2023-12-01T00:00:00.000000",
+ "2024-01-01T00:00:00.000000",
+ "2024-02-01T00:00:00.000000",
+ "2024-03-01T00:00:00.000000",
+ "2024-04-01T00:00:00.000000",
+ "2024-05-01T00:00:00.000000",
+ "2024-06-01T00:00:00.000000",
+ "2024-07-01T00:00:00.000000",
+ "2024-08-01T00:00:00.000000",
+ "2024-09-01T00:00:00.000000",
+ "2024-10-01T00:00:00.000000",
+ "2024-11-01T00:00:00.000000",
+ "2024-12-01T00:00:00.000000",
+ "2025-01-01T00:00:00.000000",
+ "2025-02-01T00:00:00.000000",
+ "2025-03-01T00:00:00.000000",
+ "2025-04-01T00:00:00.000000",
+ "2025-05-01T00:00:00.000000",
+ "2025-06-01T00:00:00.000000"
+ ],
+ "y": {
+ "bdata": "13tn8rYnf0ARMrznyemBQNoB1/v2E4NAAl1nc5gVhUC3Kw8tmhqGQKHUtTyZ14ZAEYzXEh5NikAPVSPsL3WMQNyt3eOYQ41AD7wPf/JGjkBCRWiKaB6PQFBYJHQsMZBATD6o+FZCkUCArcKAs8eMQIcQ7nPKkY9A9PflWB/XkEDEVMfH6UeRQPdCUAGxWJNAFOUYMZD+k0BoU/AJ292SQIAsPIBhNpRAvLKmc0fIlkAWzBKW3dKZQB7B2CNMNJlA2S0qb+MEmUAJEGQNvaiUQKaNIhIhN4xAiaMccJjyjEBl2lM56dOIQF3rgxlp5YhA2kakjEddi0CBxTgl27iLQJNFz55LOoxAEgN5/0kxjEAUd/KEPnGMQHyVeTp6No1ApB/dRQcVikBgNhmuyQiIQBo7Je7QZ4pA2kacit28iUCyR8l+srWIQB3GKsbZ+4dAkRIcSOffiEBm5hZxwHqIQGBJ31XqeolA5cFWU7bIiUCoCg/0WRiMQEgb/h77BoxAIJFkKVTRiUCkRByi1TGKQGmvT2S+5IpAo5zOKp/Bi0Cssz2m23SNQFWwDB81KJBAE3QD9X2zkUApLm2zG5+RQPYmAwYnGJBAzadrmEytkEA4IWXXloSQQCyUWMprqY5A5K/SKk42jEBdmkv6BjSNQA==",
+ "dtype": "f8"
+ }
+ },
+ {
+ "hovertemplate": "newest price available at 2025-08
$%{y:.0f}",
+ "marker": {
+ "color": "#1baf7a",
+ "line": {
+ "color": "#fcfcfb",
+ "width": 2
+ },
+ "size": 9
+ },
+ "mode": "markers+text",
+ "showlegend": false,
+ "text": [
+ " 2025-08"
+ ],
+ "textfont": {
+ "color": "#0b0b0b",
+ "size": 10
+ },
+ "textposition": "middle right",
+ "type": "scatter",
+ "x": [
+ "2025-06-01T00:00:00"
+ ],
+ "y": [
+ 934.5034070879444
+ ]
+ }
+ ],
+ "layout": {
+ "font": {
+ "color": "#52514e",
+ "size": 12
+ },
+ "height": 560,
+ "hovermode": "x unified",
+ "legend": {
+ "orientation": "h",
+ "x": 0,
+ "xanchor": "left",
+ "y": 1.02,
+ "yanchor": "bottom"
+ },
+ "margin": {
+ "b": 55,
+ "l": 70,
+ "r": 110,
+ "t": 70
+ },
+ "paper_bgcolor": "#fcfcfb",
+ "plot_bgcolor": "#fcfcfb",
+ "template": {
+ "data": {
+ "bar": [
+ {
+ "error_x": {
+ "color": "#2a3f5f"
+ },
+ "error_y": {
+ "color": "#2a3f5f"
+ },
+ "marker": {
+ "line": {
+ "color": "#E5ECF6",
+ "width": 0.5
+ },
+ "pattern": {
+ "fillmode": "overlay",
+ "size": 10,
+ "solidity": 0.2
+ }
+ },
+ "type": "bar"
+ }
+ ],
+ "barpolar": [
+ {
+ "marker": {
+ "line": {
+ "color": "#E5ECF6",
+ "width": 0.5
+ },
+ "pattern": {
+ "fillmode": "overlay",
+ "size": 10,
+ "solidity": 0.2
+ }
+ },
+ "type": "barpolar"
+ }
+ ],
+ "carpet": [
+ {
+ "aaxis": {
+ "endlinecolor": "#2a3f5f",
+ "gridcolor": "white",
+ "linecolor": "white",
+ "minorgridcolor": "white",
+ "startlinecolor": "#2a3f5f"
+ },
+ "baxis": {
+ "endlinecolor": "#2a3f5f",
+ "gridcolor": "white",
+ "linecolor": "white",
+ "minorgridcolor": "white",
+ "startlinecolor": "#2a3f5f"
+ },
+ "type": "carpet"
+ }
+ ],
+ "choropleth": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "type": "choropleth"
+ }
+ ],
+ "contour": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "colorscale": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "type": "contour"
+ }
+ ],
+ "contourcarpet": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "type": "contourcarpet"
+ }
+ ],
+ "heatmap": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "colorscale": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "type": "heatmap"
+ }
+ ],
+ "histogram": [
+ {
+ "marker": {
+ "pattern": {
+ "fillmode": "overlay",
+ "size": 10,
+ "solidity": 0.2
+ }
+ },
+ "type": "histogram"
+ }
+ ],
+ "histogram2d": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "colorscale": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "type": "histogram2d"
+ }
+ ],
+ "histogram2dcontour": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "colorscale": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "type": "histogram2dcontour"
+ }
+ ],
+ "mesh3d": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "type": "mesh3d"
+ }
+ ],
+ "parcoords": [
+ {
+ "line": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "parcoords"
+ }
+ ],
+ "pie": [
+ {
+ "automargin": true,
+ "type": "pie"
+ }
+ ],
+ "scatter": [
+ {
+ "fillpattern": {
+ "fillmode": "overlay",
+ "size": 10,
+ "solidity": 0.2
+ },
+ "type": "scatter"
+ }
+ ],
+ "scatter3d": [
+ {
+ "line": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scatter3d"
+ }
+ ],
+ "scattercarpet": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scattercarpet"
+ }
+ ],
+ "scattergeo": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scattergeo"
+ }
+ ],
+ "scattergl": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scattergl"
+ }
+ ],
+ "scattermap": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scattermap"
+ }
+ ],
+ "scattermapbox": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scattermapbox"
+ }
+ ],
+ "scatterpolar": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scatterpolar"
+ }
+ ],
+ "scatterpolargl": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scatterpolargl"
+ }
+ ],
+ "scatterternary": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scatterternary"
+ }
+ ],
+ "surface": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "colorscale": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "type": "surface"
+ }
+ ],
+ "table": [
+ {
+ "cells": {
+ "fill": {
+ "color": "#EBF0F8"
+ },
+ "line": {
+ "color": "white"
+ }
+ },
+ "header": {
+ "fill": {
+ "color": "#C8D4E3"
+ },
+ "line": {
+ "color": "white"
+ }
+ },
+ "type": "table"
+ }
+ ]
+ },
+ "layout": {
+ "annotationdefaults": {
+ "arrowcolor": "#2a3f5f",
+ "arrowhead": 0,
+ "arrowwidth": 1
+ },
+ "autotypenumbers": "strict",
+ "coloraxis": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "colorscale": {
+ "diverging": [
+ [
+ 0,
+ "#8e0152"
+ ],
+ [
+ 0.1,
+ "#c51b7d"
+ ],
+ [
+ 0.2,
+ "#de77ae"
+ ],
+ [
+ 0.3,
+ "#f1b6da"
+ ],
+ [
+ 0.4,
+ "#fde0ef"
+ ],
+ [
+ 0.5,
+ "#f7f7f7"
+ ],
+ [
+ 0.6,
+ "#e6f5d0"
+ ],
+ [
+ 0.7,
+ "#b8e186"
+ ],
+ [
+ 0.8,
+ "#7fbc41"
+ ],
+ [
+ 0.9,
+ "#4d9221"
+ ],
+ [
+ 1,
+ "#276419"
+ ]
+ ],
+ "sequential": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "sequentialminus": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ]
+ },
+ "colorway": [
+ "#636efa",
+ "#EF553B",
+ "#00cc96",
+ "#ab63fa",
+ "#FFA15A",
+ "#19d3f3",
+ "#FF6692",
+ "#B6E880",
+ "#FF97FF",
+ "#FECB52"
+ ],
+ "font": {
+ "color": "#2a3f5f"
+ },
+ "geo": {
+ "bgcolor": "white",
+ "lakecolor": "white",
+ "landcolor": "#E5ECF6",
+ "showlakes": true,
+ "showland": true,
+ "subunitcolor": "white"
+ },
+ "hoverlabel": {
+ "align": "left"
+ },
+ "hovermode": "closest",
+ "mapbox": {
+ "style": "light"
+ },
+ "paper_bgcolor": "white",
+ "plot_bgcolor": "#E5ECF6",
+ "polar": {
+ "angularaxis": {
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": ""
+ },
+ "bgcolor": "#E5ECF6",
+ "radialaxis": {
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": ""
+ }
+ },
+ "scene": {
+ "xaxis": {
+ "backgroundcolor": "#E5ECF6",
+ "gridcolor": "white",
+ "gridwidth": 2,
+ "linecolor": "white",
+ "showbackground": true,
+ "ticks": "",
+ "zerolinecolor": "white"
+ },
+ "yaxis": {
+ "backgroundcolor": "#E5ECF6",
+ "gridcolor": "white",
+ "gridwidth": 2,
+ "linecolor": "white",
+ "showbackground": true,
+ "ticks": "",
+ "zerolinecolor": "white"
+ },
+ "zaxis": {
+ "backgroundcolor": "#E5ECF6",
+ "gridcolor": "white",
+ "gridwidth": 2,
+ "linecolor": "white",
+ "showbackground": true,
+ "ticks": "",
+ "zerolinecolor": "white"
+ }
+ },
+ "shapedefaults": {
+ "line": {
+ "color": "#2a3f5f"
+ }
+ },
+ "ternary": {
+ "aaxis": {
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": ""
+ },
+ "baxis": {
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": ""
+ },
+ "bgcolor": "#E5ECF6",
+ "caxis": {
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": ""
+ }
+ },
+ "title": {
+ "x": 0.05
+ },
+ "xaxis": {
+ "automargin": true,
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": "",
+ "title": {
+ "standoff": 15
+ },
+ "zerolinecolor": "white",
+ "zerolinewidth": 2
+ },
+ "yaxis": {
+ "automargin": true,
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": "",
+ "title": {
+ "standoff": 15
+ },
+ "zerolinecolor": "white",
+ "zerolinewidth": 2
+ }
+ }
+ },
+ "title": {
+ "font": {
+ "color": "#0b0b0b",
+ "size": 17
+ },
+ "text": "What the model can see at each cutoff, vs what really happened"
+ },
+ "xaxis": {
+ "gridcolor": "#e6e5e1",
+ "linecolor": "#e6e5e1",
+ "showspikes": false,
+ "zeroline": false
+ },
+ "yaxis": {
+ "gridcolor": "#e6e5e1",
+ "linecolor": "#e6e5e1",
+ "showspikes": false,
+ "title": {
+ "text": "USD / metric ton"
+ },
+ "zeroline": false
+ }
+ }
+ }
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "plot_information_gap(svc, PALM_OIL_SERIES_ID)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "a9e40a49",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-08-06T19:18:04.652672Z",
+ "iopub.status.busy": "2026-08-06T19:18:04.652338Z",
+ "iopub.status.idle": "2026-08-06T19:18:04.679538Z",
+ "shell.execute_reply": "2026-08-06T19:18:04.677568Z"
+ }
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " | \n",
+ " cutoff | \n",
+ " kind | \n",
+ " newest price | \n",
+ " gap (months) | \n",
+ " h=1 really means | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " | 0 | \n",
+ " 2021-05 | \n",
+ " event | \n",
+ " 2021-03 | \n",
+ " 2 | \n",
+ " 3 months past last data | \n",
+ "
\n",
+ " \n",
+ " | 1 | \n",
+ " 2022-01 | \n",
+ " event | \n",
+ " 2021-11 | \n",
+ " 2 | \n",
+ " 3 months past last data | \n",
+ "
\n",
+ " \n",
+ " | 2 | \n",
+ " 2023-04 | \n",
+ " event | \n",
+ " 2023-02 | \n",
+ " 2 | \n",
+ " 3 months past last data | \n",
+ "
\n",
+ " \n",
+ " | 3 | \n",
+ " 2024-09 | \n",
+ " event | \n",
+ " 2024-07 | \n",
+ " 2 | \n",
+ " 3 months past last data | \n",
+ "
\n",
+ " \n",
+ " | 4 | \n",
+ " 2023-07 | \n",
+ " quiet | \n",
+ " 2023-05 | \n",
+ " 2 | \n",
+ " 3 months past last data | \n",
+ "
\n",
+ " \n",
+ " | 5 | \n",
+ " 2024-11 | \n",
+ " quiet | \n",
+ " 2024-09 | \n",
+ " 2 | \n",
+ " 3 months past last data | \n",
+ "
\n",
+ " \n",
+ " | 6 | \n",
+ " 2025-08 | \n",
+ " quiet | \n",
+ " 2025-06 | \n",
+ " 2 | \n",
+ " 3 months past last data | \n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ " cutoff kind newest price gap (months) h=1 really means\n",
+ "0 2021-05 event 2021-03 2 3 months past last data\n",
+ "1 2022-01 event 2021-11 2 3 months past last data\n",
+ "2 2023-04 event 2023-02 2 3 months past last data\n",
+ "3 2024-09 event 2024-07 2 3 months past last data\n",
+ "4 2023-07 quiet 2023-05 2 3 months past last data\n",
+ "5 2024-11 quiet 2024-09 2 3 months past last data\n",
+ "6 2025-08 quiet 2025-06 2 3 months past last data"
+ ]
+ },
+ "execution_count": 7,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# The gap at every candidate cutoff, as a table.\n",
+ "rows = []\n",
+ "for c in DEFAULT_CUTOFFS:\n",
+ " seen = svc.get_series(PALM_OIL_SERIES_ID, as_of=c.timestamp.to_pydatetime())\n",
+ " last = seen.timestamp.max()\n",
+ " gap = (c.timestamp.year - last.year) * 12 + (c.timestamp.month - last.month)\n",
+ " rows.append(\n",
+ " {\n",
+ " \"cutoff\": c.date[:7],\n",
+ " \"kind\": c.kind,\n",
+ " \"newest price\": f\"{last:%Y-%m}\",\n",
+ " \"gap (months)\": gap,\n",
+ " \"h=1 really means\": f\"{gap + 1} months past last data\",\n",
+ " }\n",
+ " )\n",
+ "pd.DataFrame(rows)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5831096c",
+ "metadata": {},
+ "source": [
+ "A nominal horizon of 1 is really a **3-month** extrapolation once the 2-month gap is\n",
+ "counted. Horizons 1–6 therefore span 3 to 8 months of real forecast distance — worth\n",
+ "stating explicitly in any writeup, or `h=1` reads as an easy nowcast when it isn't.\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "fe848bd4",
+ "metadata": {},
+ "source": [
+ "---\n",
+ "## 6. The rest of the edible-oil complex\n",
+ "\n",
+ "Palm oil against the three other IMF oils. Same units, same release calendar, same\n",
+ "leak-safe handling — so they are cheap covariates if they carry signal.\n",
+ "\n",
+ "Click legend entries to isolate pairs and judge whether they move together.\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "36e12882",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-08-06T19:18:04.683156Z",
+ "iopub.status.busy": "2026-08-06T19:18:04.682848Z",
+ "iopub.status.idle": "2026-08-06T19:18:04.736109Z",
+ "shell.execute_reply": "2026-08-06T19:18:04.734618Z"
+ }
+ },
+ "outputs": [
+ {
+ "data": {
+ "application/vnd.plotly.v1+json": {
+ "config": {
+ "plotlyServerURL": "https://plot.ly"
+ },
+ "data": [
+ {
+ "hovertemplate": "Palm oil
%{x|%b %Y} $%{y:.0f}",
+ "line": {
+ "color": "#2a78d6",
+ "width": 2
+ },
+ "mode": "lines",
+ "name": "Palm oil",
+ "type": "scatter",
+ "x": [
+ "2015-01-01T00:00:00.000000",
+ "2015-02-01T00:00:00.000000",
+ "2015-03-01T00:00:00.000000",
+ "2015-04-01T00:00:00.000000",
+ "2015-05-01T00:00:00.000000",
+ "2015-06-01T00:00:00.000000",
+ "2015-07-01T00:00:00.000000",
+ "2015-08-01T00:00:00.000000",
+ "2015-09-01T00:00:00.000000",
+ "2015-10-01T00:00:00.000000",
+ "2015-11-01T00:00:00.000000",
+ "2015-12-01T00:00:00.000000",
+ "2016-01-01T00:00:00.000000",
+ "2016-02-01T00:00:00.000000",
+ "2016-03-01T00:00:00.000000",
+ "2016-04-01T00:00:00.000000",
+ "2016-05-01T00:00:00.000000",
+ "2016-06-01T00:00:00.000000",
+ "2016-07-01T00:00:00.000000",
+ "2016-08-01T00:00:00.000000",
+ "2016-09-01T00:00:00.000000",
+ "2016-10-01T00:00:00.000000",
+ "2016-11-01T00:00:00.000000",
+ "2016-12-01T00:00:00.000000",
+ "2017-01-01T00:00:00.000000",
+ "2017-02-01T00:00:00.000000",
+ "2017-03-01T00:00:00.000000",
+ "2017-04-01T00:00:00.000000",
+ "2017-05-01T00:00:00.000000",
+ "2017-06-01T00:00:00.000000",
+ "2017-07-01T00:00:00.000000",
+ "2017-08-01T00:00:00.000000",
+ "2017-09-01T00:00:00.000000",
+ "2017-10-01T00:00:00.000000",
+ "2017-11-01T00:00:00.000000",
+ "2017-12-01T00:00:00.000000",
+ "2018-01-01T00:00:00.000000",
+ "2018-02-01T00:00:00.000000",
+ "2018-03-01T00:00:00.000000",
+ "2018-04-01T00:00:00.000000",
+ "2018-05-01T00:00:00.000000",
+ "2018-06-01T00:00:00.000000",
+ "2018-07-01T00:00:00.000000",
+ "2018-08-01T00:00:00.000000",
+ "2018-09-01T00:00:00.000000",
+ "2018-10-01T00:00:00.000000",
+ "2018-11-01T00:00:00.000000",
+ "2018-12-01T00:00:00.000000",
+ "2019-01-01T00:00:00.000000",
+ "2019-02-01T00:00:00.000000",
+ "2019-03-01T00:00:00.000000",
+ "2019-04-01T00:00:00.000000",
+ "2019-05-01T00:00:00.000000",
+ "2019-06-01T00:00:00.000000",
+ "2019-07-01T00:00:00.000000",
+ "2019-08-01T00:00:00.000000",
+ "2019-09-01T00:00:00.000000",
+ "2019-10-01T00:00:00.000000",
+ "2019-11-01T00:00:00.000000",
+ "2019-12-01T00:00:00.000000",
+ "2020-01-01T00:00:00.000000",
+ "2020-02-01T00:00:00.000000",
+ "2020-03-01T00:00:00.000000",
+ "2020-04-01T00:00:00.000000",
+ "2020-05-01T00:00:00.000000",
+ "2020-06-01T00:00:00.000000",
+ "2020-07-01T00:00:00.000000",
+ "2020-08-01T00:00:00.000000",
+ "2020-09-01T00:00:00.000000",
+ "2020-10-01T00:00:00.000000",
+ "2020-11-01T00:00:00.000000",
+ "2020-12-01T00:00:00.000000",
+ "2021-01-01T00:00:00.000000",
+ "2021-02-01T00:00:00.000000",
+ "2021-03-01T00:00:00.000000",
+ "2021-04-01T00:00:00.000000",
+ "2021-05-01T00:00:00.000000",
+ "2021-06-01T00:00:00.000000",
+ "2021-07-01T00:00:00.000000",
+ "2021-08-01T00:00:00.000000",
+ "2021-09-01T00:00:00.000000",
+ "2021-10-01T00:00:00.000000",
+ "2021-11-01T00:00:00.000000",
+ "2021-12-01T00:00:00.000000",
+ "2022-01-01T00:00:00.000000",
+ "2022-02-01T00:00:00.000000",
+ "2022-03-01T00:00:00.000000",
+ "2022-04-01T00:00:00.000000",
+ "2022-05-01T00:00:00.000000",
+ "2022-06-01T00:00:00.000000",
+ "2022-07-01T00:00:00.000000",
+ "2022-08-01T00:00:00.000000",
+ "2022-09-01T00:00:00.000000",
+ "2022-10-01T00:00:00.000000",
+ "2022-11-01T00:00:00.000000",
+ "2022-12-01T00:00:00.000000",
+ "2023-01-01T00:00:00.000000",
+ "2023-02-01T00:00:00.000000",
+ "2023-03-01T00:00:00.000000",
+ "2023-04-01T00:00:00.000000",
+ "2023-05-01T00:00:00.000000",
+ "2023-06-01T00:00:00.000000",
+ "2023-07-01T00:00:00.000000",
+ "2023-08-01T00:00:00.000000",
+ "2023-09-01T00:00:00.000000",
+ "2023-10-01T00:00:00.000000",
+ "2023-11-01T00:00:00.000000",
+ "2023-12-01T00:00:00.000000",
+ "2024-01-01T00:00:00.000000",
+ "2024-02-01T00:00:00.000000",
+ "2024-03-01T00:00:00.000000",
+ "2024-04-01T00:00:00.000000",
+ "2024-05-01T00:00:00.000000",
+ "2024-06-01T00:00:00.000000",
+ "2024-07-01T00:00:00.000000",
+ "2024-08-01T00:00:00.000000",
+ "2024-09-01T00:00:00.000000",
+ "2024-10-01T00:00:00.000000",
+ "2024-11-01T00:00:00.000000",
+ "2024-12-01T00:00:00.000000",
+ "2025-01-01T00:00:00.000000",
+ "2025-02-01T00:00:00.000000",
+ "2025-03-01T00:00:00.000000",
+ "2025-04-01T00:00:00.000000",
+ "2025-05-01T00:00:00.000000",
+ "2025-06-01T00:00:00.000000",
+ "2025-07-01T00:00:00.000000",
+ "2025-08-01T00:00:00.000000",
+ "2025-09-01T00:00:00.000000",
+ "2025-10-01T00:00:00.000000",
+ "2025-11-01T00:00:00.000000",
+ "2025-12-01T00:00:00.000000",
+ "2026-01-01T00:00:00.000000",
+ "2026-02-01T00:00:00.000000",
+ "2026-03-01T00:00:00.000000",
+ "2026-04-01T00:00:00.000000",
+ "2026-05-01T00:00:00.000000",
+ "2026-06-01T00:00:00.000000"
+ ],
+ "y": {
+ "bdata": "Wg8vIscMhECSxX5mBtODQAkoCio9/YJAwM8/qU1+gkBXmUQdLMuCQO0PdGQ884JAvaRiAHX9gUDDHP9p10p+QNlzGwPMN35ArDZIHvmRgEDqlwQwnnJ/QET/9g7SRIBAwbhOnvScgEAEXWiZNJ+CQJSLsNGKyINAvl0y/wBDhUABplSwhCSEQIx2W9CfU4NAbIZmZIVBgkC7Z0tXBMOEQLH8I9ZAo4VAfxn00ZZbhEBtGIoP+++EQFnNv8UMPoZAFY0LkOizhkBrn6xIWRaGQMIFa2pluoRAX6EnTKl5g0ASXwC+FXyEQMFm2uNzaYNAeAlJCURJg0AGwAVEjm6DQMmWCHINrYRAk5hBFktBhED/i/usgtyDQJGTGeNhhoJAw52vrsfKg0BAEVy4jBeEQABOcKp4goNAVFLEwwFQg0B/F5hSLPCCQO54pBUvN4JAqODLUqrRgEAvz/mv0rKAQJIfOSzTZoBAU6vGWLKtf0ASiUH/RsF7QPW5YM8F53xAdH5AOjk5gEBAxc2dTVKAQOhdQfzeyn5A/JAN0o90f0ACfEZrkIF9QGhaAXwIqn1Ae4dU/G4jfUDRBWMGvsl/QNlP1XezrX9Aob/YOilcgEDSFesWbCaDQBpJsWlEaoVARQsNUK+0hkCqQEpqBBKEQJ1XoAI5b4FAx7HSEcszgEDXe2fytid/QBEyvOfJ6YFA2gHX+/YTg0ACXWdzmBWFQLcrDy2aGoZAodS1PJnXhkARjNcSHk2KQA9VI+wvdYxA3K3d45hDjUAPvA9/8kaOQEJFaIpoHo9AUFgkdCwxkEBMPqj4VkKRQICtwoCzx4xAhxDuc8qRj0D09+VYH9eQQMRUx8fpR5FA90JQAbFYk0AU5RgxkP6TQGhT8Anb3ZJAgCw8gGE2lEC8sqZzR8iWQBbMEpbd0plAHsHYI0w0mUDZLSpv4wSZQAkQZA29qJRApo0iEiE3jECJoxxwmPKMQGXaUznp04hAXeuDGWnliEDaRqSMR12LQIHFOCXbuItAk0XPnks6jEASA3n/STGMQBR38oQ+cYxAfJV5Ono2jUCkH91FBxWKQGA2Ga7JCIhAGjsl7tBnikDaRpyK3byJQLJHyX6ytYhAHcYqxtn7h0CREhxI59+IQGbmFnHAeohAYEnfVep6iUDlwVZTtsiJQKgKD/RZGIxASBv+HvsGjEAgkWQpVNGJQKREHKLVMYpAaa9PZL7kikCjnM4qn8GLQKyzPabbdI1AVbAMHzUokEATdAP1fbORQCkubbMbn5FA9iYDBicYkEDNp2uYTK2QQDghZdeWhJBALJRYymupjkDkr9IqTjaMQF2aS/oGNI1AMI2kIlMYjUCCl5M2DQiQQJziUeCZK5BABxCaHyA5kEAmMK/uc4iOQOEV/h/MqY5Aiu5kDLxij0CYzNzVEieQQDduqiq2hJFAH8USuaTFkUBUwkdmG6iRQLPAKHG5UpFA",
+ "dtype": "f8"
+ }
+ },
+ {
+ "hoverinfo": "skip",
+ "marker": {
+ "color": "#2a78d6",
+ "line": {
+ "color": "#fcfcfb",
+ "width": 2
+ },
+ "size": 9
+ },
+ "mode": "markers+text",
+ "showlegend": false,
+ "text": [
+ " Palm oil"
+ ],
+ "textfont": {
+ "color": "#0b0b0b",
+ "size": 10
+ },
+ "textposition": "middle right",
+ "type": "scatter",
+ "x": [
+ "2026-06-01T00:00:00"
+ ],
+ "y": [
+ 1108.681095730554
+ ]
+ },
+ {
+ "hovertemplate": "Soybean oil
%{x|%b %Y} $%{y:.0f}",
+ "line": {
+ "color": "#eb6834",
+ "width": 2
+ },
+ "mode": "lines",
+ "name": "Soybean oil",
+ "type": "scatter",
+ "x": [
+ "2015-01-01T00:00:00.000000",
+ "2015-02-01T00:00:00.000000",
+ "2015-03-01T00:00:00.000000",
+ "2015-04-01T00:00:00.000000",
+ "2015-05-01T00:00:00.000000",
+ "2015-06-01T00:00:00.000000",
+ "2015-07-01T00:00:00.000000",
+ "2015-08-01T00:00:00.000000",
+ "2015-09-01T00:00:00.000000",
+ "2015-10-01T00:00:00.000000",
+ "2015-11-01T00:00:00.000000",
+ "2015-12-01T00:00:00.000000",
+ "2016-01-01T00:00:00.000000",
+ "2016-02-01T00:00:00.000000",
+ "2016-03-01T00:00:00.000000",
+ "2016-04-01T00:00:00.000000",
+ "2016-05-01T00:00:00.000000",
+ "2016-06-01T00:00:00.000000",
+ "2016-07-01T00:00:00.000000",
+ "2016-08-01T00:00:00.000000",
+ "2016-09-01T00:00:00.000000",
+ "2016-10-01T00:00:00.000000",
+ "2016-11-01T00:00:00.000000",
+ "2016-12-01T00:00:00.000000",
+ "2017-01-01T00:00:00.000000",
+ "2017-02-01T00:00:00.000000",
+ "2017-03-01T00:00:00.000000",
+ "2017-04-01T00:00:00.000000",
+ "2017-05-01T00:00:00.000000",
+ "2017-06-01T00:00:00.000000",
+ "2017-07-01T00:00:00.000000",
+ "2017-08-01T00:00:00.000000",
+ "2017-09-01T00:00:00.000000",
+ "2017-10-01T00:00:00.000000",
+ "2017-11-01T00:00:00.000000",
+ "2017-12-01T00:00:00.000000",
+ "2018-01-01T00:00:00.000000",
+ "2018-02-01T00:00:00.000000",
+ "2018-03-01T00:00:00.000000",
+ "2018-04-01T00:00:00.000000",
+ "2018-05-01T00:00:00.000000",
+ "2018-06-01T00:00:00.000000",
+ "2018-07-01T00:00:00.000000",
+ "2018-08-01T00:00:00.000000",
+ "2018-09-01T00:00:00.000000",
+ "2018-10-01T00:00:00.000000",
+ "2018-11-01T00:00:00.000000",
+ "2018-12-01T00:00:00.000000",
+ "2019-01-01T00:00:00.000000",
+ "2019-02-01T00:00:00.000000",
+ "2019-03-01T00:00:00.000000",
+ "2019-04-01T00:00:00.000000",
+ "2019-05-01T00:00:00.000000",
+ "2019-06-01T00:00:00.000000",
+ "2019-07-01T00:00:00.000000",
+ "2019-08-01T00:00:00.000000",
+ "2019-09-01T00:00:00.000000",
+ "2019-10-01T00:00:00.000000",
+ "2019-11-01T00:00:00.000000",
+ "2019-12-01T00:00:00.000000",
+ "2020-01-01T00:00:00.000000",
+ "2020-02-01T00:00:00.000000",
+ "2020-03-01T00:00:00.000000",
+ "2020-04-01T00:00:00.000000",
+ "2020-05-01T00:00:00.000000",
+ "2020-06-01T00:00:00.000000",
+ "2020-07-01T00:00:00.000000",
+ "2020-08-01T00:00:00.000000",
+ "2020-09-01T00:00:00.000000",
+ "2020-10-01T00:00:00.000000",
+ "2020-11-01T00:00:00.000000",
+ "2020-12-01T00:00:00.000000",
+ "2021-01-01T00:00:00.000000",
+ "2021-02-01T00:00:00.000000",
+ "2021-03-01T00:00:00.000000",
+ "2021-04-01T00:00:00.000000",
+ "2021-05-01T00:00:00.000000",
+ "2021-06-01T00:00:00.000000",
+ "2021-07-01T00:00:00.000000",
+ "2021-08-01T00:00:00.000000",
+ "2021-09-01T00:00:00.000000",
+ "2021-10-01T00:00:00.000000",
+ "2021-11-01T00:00:00.000000",
+ "2021-12-01T00:00:00.000000",
+ "2022-01-01T00:00:00.000000",
+ "2022-02-01T00:00:00.000000",
+ "2022-03-01T00:00:00.000000",
+ "2022-04-01T00:00:00.000000",
+ "2022-05-01T00:00:00.000000",
+ "2022-06-01T00:00:00.000000",
+ "2022-07-01T00:00:00.000000",
+ "2022-08-01T00:00:00.000000",
+ "2022-09-01T00:00:00.000000",
+ "2022-10-01T00:00:00.000000",
+ "2022-11-01T00:00:00.000000",
+ "2022-12-01T00:00:00.000000",
+ "2023-01-01T00:00:00.000000",
+ "2023-02-01T00:00:00.000000",
+ "2023-03-01T00:00:00.000000",
+ "2023-04-01T00:00:00.000000",
+ "2023-05-01T00:00:00.000000",
+ "2023-06-01T00:00:00.000000",
+ "2023-07-01T00:00:00.000000",
+ "2023-08-01T00:00:00.000000",
+ "2023-09-01T00:00:00.000000",
+ "2023-10-01T00:00:00.000000",
+ "2023-11-01T00:00:00.000000",
+ "2023-12-01T00:00:00.000000",
+ "2024-01-01T00:00:00.000000",
+ "2024-02-01T00:00:00.000000",
+ "2024-03-01T00:00:00.000000",
+ "2024-04-01T00:00:00.000000",
+ "2024-05-01T00:00:00.000000",
+ "2024-06-01T00:00:00.000000",
+ "2024-07-01T00:00:00.000000",
+ "2024-08-01T00:00:00.000000",
+ "2024-09-01T00:00:00.000000",
+ "2024-10-01T00:00:00.000000",
+ "2024-11-01T00:00:00.000000",
+ "2024-12-01T00:00:00.000000",
+ "2025-01-01T00:00:00.000000",
+ "2025-02-01T00:00:00.000000",
+ "2025-03-01T00:00:00.000000",
+ "2025-04-01T00:00:00.000000",
+ "2025-05-01T00:00:00.000000",
+ "2025-06-01T00:00:00.000000",
+ "2025-07-01T00:00:00.000000",
+ "2025-08-01T00:00:00.000000",
+ "2025-09-01T00:00:00.000000",
+ "2025-10-01T00:00:00.000000",
+ "2025-11-01T00:00:00.000000",
+ "2025-12-01T00:00:00.000000",
+ "2026-01-01T00:00:00.000000",
+ "2026-02-01T00:00:00.000000",
+ "2026-03-01T00:00:00.000000",
+ "2026-04-01T00:00:00.000000",
+ "2026-05-01T00:00:00.000000",
+ "2026-06-01T00:00:00.000000"
+ ],
+ "y": {
+ "bdata": "MHQ2Lg0fhkALF9F/fc+FQKYKRiV1W4VAoP5f4GKdhUCMJyd/7GOGQK4oJQRLEIdAp+mzA06+hUA4rxgd+qWDQPaQA4kAcoJAke18P3V+g0DZbzfS4zWDQCDrqdWXKYVADNlbzTSfhEAB3CxePHeFQIWWdf/YToZAnggjeEBkh0D0mYXOQRiGQIWy8PXV/IVAW3XC8ObuhEDHB8qc5j2GQF71Pz7klIZAKpWzNguqh0Cb/GqjUSOIQAosFWsMAolAKa1kbA0fiEDqxs2FYDeHQDRBqi05m4ZAVbRLMGu6hUBsfCb7h1CGQC6thsSdBoZAOaAA5coVh0Ba/NAAGlWHQNSmoJE+nYdA1EZ1OvAlh0AVLn1A9rSHQArdgCqJ1IZAtadPW0+1hkDg3iBLBCuGQI52HSzg8YVAXHtiXO+YhUDW5CmriVuFQLMM3GqCi4RAzwUyfEaBg0DKw3FYb3iDQA5t9Em3QYNAxpExchLsg0BhOhia1xODQKLyCp8Ib4NAmv6+12bng0B2UesP6dKEQK97KxKTKIRA+Ar4Ioesg0BnYORlLayCQJVP6iZsMINA662BrfJJg0C/DMaIRKmDQNoZS/0BA4RAh6XfdeDVhEBKStXRDFSFQDxV1R/2coZAn0AZZTrAhkA96Vbk5taEQFcHQNx1h4JA9YXRLmMNgkBmxGBZJEuCQHU+PEswN4NAPQ/uzvoXhEAGLeMk0OmFQOdUx0GdJ4dAkGeXb135hkAU7SqkbGWJQKqdYWpLiotA3fEa09zkjUDOv3KT8yuQQBUcSWhNoZJA58Qe2gf3k0D4y5LpnAyXQDwUBfp0eZZA9UtwVcKulkAXSianxm2VQKT6D2uOtZNAkfE7YHEOlUA8fawKm2eUQFNaf0tQAJNAo6yEkyHglED9C/XZfhWXQEIG8uyyIZpAYWgKtC9Hm0CjWdk+dLucQLFqEOb2QJpAN7nbtNdslUAm/QbD5bqXQIdT5uYrlZdAtZvcrhUwmED8VYDvZuCZQEAXDRmfe5ZAOWBXkxeSlUCTDVr+WvOUQKPoyj0phpNABuD6C3CVkkCA7vXp9BiRQP9AuW1vTZNAkvAofvhql0DwwvBvx/eWQObrMvwHdZVAGttrQZ/UkkBsBOJ1XdSRQDXXwZd7HZFAkZx7hzBUkEAUKpCvFnGPQHhg7RVDRZBA4GjHDT+zj0D3wtwDToyOQDTolfzJIY5A8Q7wpAX0j0BSmzi5f4SMQGTGg1e504xAMP1G99rcjUD/ltZ4QaWOQDlGskfIOYxAPARFVjI0jkBCmgwi/ZOPQK65Dr4cRo1AKZXwhL5fkEDeE/UPZ82QQNq39PkmQpJAXKZPnu1ukkAXFxGb9/uRQAQ6OxHFT5FAsxLZ4KAmkUCEU/KDxCuRQKJRofa04ZBA0K5r0GNakUDAMeOov2WTQEH5fkHTL5ZAswpVaGNWl0BHs3IFsz+ZQC18cllutZhA",
+ "dtype": "f8"
+ }
+ },
+ {
+ "hoverinfo": "skip",
+ "marker": {
+ "color": "#eb6834",
+ "line": {
+ "color": "#fcfcfb",
+ "width": 2
+ },
+ "size": 9
+ },
+ "mode": "markers+text",
+ "showlegend": false,
+ "text": [
+ " Soybean oil"
+ ],
+ "textfont": {
+ "color": "#0b0b0b",
+ "size": 10
+ },
+ "textposition": "middle right",
+ "type": "scatter",
+ "x": [
+ "2026-06-01T00:00:00"
+ ],
+ "y": [
+ 1581.357763089017
+ ]
+ },
+ {
+ "hovertemplate": "Sunflower oil
%{x|%b %Y} $%{y:.0f}",
+ "line": {
+ "color": "#1baf7a",
+ "width": 2
+ },
+ "mode": "lines",
+ "name": "Sunflower oil",
+ "type": "scatter",
+ "x": [
+ "2015-01-01T00:00:00.000000",
+ "2015-02-01T00:00:00.000000",
+ "2015-03-01T00:00:00.000000",
+ "2015-04-01T00:00:00.000000",
+ "2015-05-01T00:00:00.000000",
+ "2015-06-01T00:00:00.000000",
+ "2015-07-01T00:00:00.000000",
+ "2015-08-01T00:00:00.000000",
+ "2015-09-01T00:00:00.000000",
+ "2015-10-01T00:00:00.000000",
+ "2015-11-01T00:00:00.000000",
+ "2015-12-01T00:00:00.000000",
+ "2016-01-01T00:00:00.000000",
+ "2016-02-01T00:00:00.000000",
+ "2016-03-01T00:00:00.000000",
+ "2016-04-01T00:00:00.000000",
+ "2016-05-01T00:00:00.000000",
+ "2016-06-01T00:00:00.000000",
+ "2016-07-01T00:00:00.000000",
+ "2016-08-01T00:00:00.000000",
+ "2016-09-01T00:00:00.000000",
+ "2016-10-01T00:00:00.000000",
+ "2016-11-01T00:00:00.000000",
+ "2016-12-01T00:00:00.000000",
+ "2017-01-01T00:00:00.000000",
+ "2017-02-01T00:00:00.000000",
+ "2017-03-01T00:00:00.000000",
+ "2017-04-01T00:00:00.000000",
+ "2017-05-01T00:00:00.000000",
+ "2017-06-01T00:00:00.000000",
+ "2017-07-01T00:00:00.000000",
+ "2017-08-01T00:00:00.000000",
+ "2017-09-01T00:00:00.000000",
+ "2017-10-01T00:00:00.000000",
+ "2017-11-01T00:00:00.000000",
+ "2017-12-01T00:00:00.000000",
+ "2018-01-01T00:00:00.000000",
+ "2018-02-01T00:00:00.000000",
+ "2018-03-01T00:00:00.000000",
+ "2018-04-01T00:00:00.000000",
+ "2018-05-01T00:00:00.000000",
+ "2018-06-01T00:00:00.000000",
+ "2018-07-01T00:00:00.000000",
+ "2018-08-01T00:00:00.000000",
+ "2018-09-01T00:00:00.000000",
+ "2018-10-01T00:00:00.000000",
+ "2018-11-01T00:00:00.000000",
+ "2018-12-01T00:00:00.000000",
+ "2019-01-01T00:00:00.000000",
+ "2019-02-01T00:00:00.000000",
+ "2019-03-01T00:00:00.000000",
+ "2019-04-01T00:00:00.000000",
+ "2019-05-01T00:00:00.000000",
+ "2019-06-01T00:00:00.000000",
+ "2019-07-01T00:00:00.000000",
+ "2019-08-01T00:00:00.000000",
+ "2019-09-01T00:00:00.000000",
+ "2019-10-01T00:00:00.000000",
+ "2019-11-01T00:00:00.000000",
+ "2019-12-01T00:00:00.000000",
+ "2020-01-01T00:00:00.000000",
+ "2020-02-01T00:00:00.000000",
+ "2020-03-01T00:00:00.000000",
+ "2020-04-01T00:00:00.000000",
+ "2020-05-01T00:00:00.000000",
+ "2020-06-01T00:00:00.000000",
+ "2020-07-01T00:00:00.000000",
+ "2020-08-01T00:00:00.000000",
+ "2020-09-01T00:00:00.000000",
+ "2020-10-01T00:00:00.000000",
+ "2020-11-01T00:00:00.000000",
+ "2020-12-01T00:00:00.000000",
+ "2021-01-01T00:00:00.000000",
+ "2021-02-01T00:00:00.000000",
+ "2021-03-01T00:00:00.000000",
+ "2021-04-01T00:00:00.000000",
+ "2021-05-01T00:00:00.000000",
+ "2021-06-01T00:00:00.000000",
+ "2021-07-01T00:00:00.000000",
+ "2021-08-01T00:00:00.000000",
+ "2021-09-01T00:00:00.000000",
+ "2021-10-01T00:00:00.000000",
+ "2021-11-01T00:00:00.000000",
+ "2021-12-01T00:00:00.000000",
+ "2022-01-01T00:00:00.000000",
+ "2022-02-01T00:00:00.000000",
+ "2022-03-01T00:00:00.000000",
+ "2022-04-01T00:00:00.000000",
+ "2022-05-01T00:00:00.000000",
+ "2022-06-01T00:00:00.000000",
+ "2022-07-01T00:00:00.000000",
+ "2022-08-01T00:00:00.000000",
+ "2022-09-01T00:00:00.000000",
+ "2022-10-01T00:00:00.000000",
+ "2022-11-01T00:00:00.000000",
+ "2022-12-01T00:00:00.000000",
+ "2023-01-01T00:00:00.000000",
+ "2023-02-01T00:00:00.000000",
+ "2023-03-01T00:00:00.000000",
+ "2023-04-01T00:00:00.000000",
+ "2023-05-01T00:00:00.000000",
+ "2023-06-01T00:00:00.000000",
+ "2023-07-01T00:00:00.000000",
+ "2023-08-01T00:00:00.000000",
+ "2023-09-01T00:00:00.000000",
+ "2023-10-01T00:00:00.000000",
+ "2023-11-01T00:00:00.000000",
+ "2023-12-01T00:00:00.000000",
+ "2024-01-01T00:00:00.000000",
+ "2024-02-01T00:00:00.000000",
+ "2024-03-01T00:00:00.000000",
+ "2024-04-01T00:00:00.000000",
+ "2024-05-01T00:00:00.000000",
+ "2024-06-01T00:00:00.000000",
+ "2024-07-01T00:00:00.000000",
+ "2024-08-01T00:00:00.000000",
+ "2024-09-01T00:00:00.000000",
+ "2024-10-01T00:00:00.000000",
+ "2024-11-01T00:00:00.000000",
+ "2024-12-01T00:00:00.000000",
+ "2025-01-01T00:00:00.000000",
+ "2025-02-01T00:00:00.000000",
+ "2025-03-01T00:00:00.000000",
+ "2025-04-01T00:00:00.000000",
+ "2025-05-01T00:00:00.000000",
+ "2025-06-01T00:00:00.000000",
+ "2025-07-01T00:00:00.000000",
+ "2025-08-01T00:00:00.000000",
+ "2025-09-01T00:00:00.000000",
+ "2025-10-01T00:00:00.000000",
+ "2025-11-01T00:00:00.000000",
+ "2025-12-01T00:00:00.000000",
+ "2026-01-01T00:00:00.000000",
+ "2026-02-01T00:00:00.000000",
+ "2026-03-01T00:00:00.000000",
+ "2026-04-01T00:00:00.000000",
+ "2026-05-01T00:00:00.000000",
+ "2026-06-01T00:00:00.000000"
+ ],
+ "y": {
+ "bdata": "KjMzM5Nhj0DBHoXrIQSOQKmqqqoK6o1Ax7u7uxtQj0AekzEZk+eQQM3MzMxsO5FA0J46GQ61kEByQzd0A6KOQEAzMzNzrY5Aa2ZmZjaMkEAuy7IsC1SQQPwCXCQn8Y9ApBd5kRfhj0CJUAd1UD6QQESs2Dxer49AP/ZjP7YRkEAvMzMz40CQQAAAAABgyo9Amxd5kdd4jkCUWvbUia+OQG5mZmZm6I5A2EEd1EEzj0BdG+i04UOPQL+7u7v7oY9Ae2ZmZuamjkAkS36xxDeOQOfGFIQ+Ro1AveQXS55fjUBWdTIcqq6NQOXMzMzsQY1AJ1IhFZK/jUDrDsy1wz6OQM0JnMCJXo5AsZmZmfmJjUDdzMzMLKSNQGlv9VbvYo1AUAJFW807jUDZ9Shcr7+NQFdERETklI1Aq9qpnZrvjUA1lQ/j/leNQLNP+qQPfYxA3czMzCwhjUCkPXUyHMmLQAXGkl9czIpAtJPhUMu9ikCxqqqqipCJQOagDupgc4lAUfyMEj/ciUAz/mmSwXmKQEF0Qzf0RopAKCIiIsJFikD8XoCLJDyLQFDhehQerotAxF+XVD6JjECWiIiIaGWMQJxyKZdyQIxAWchCFrKni0AREREREYWMQE9EREREKI5ANT9VELpBj0Bcj8L1eI2MQJqZmZn5d4pAlt93d/dxi0Bxo87mbCCMQDuEh4iIH45AxNoT4OJwjkADEqLNuSWQQCBEUVU1kpJA7RAeIoK5kkB4zf9AUBCVQBv365KKYZZALzXuOE48mEA8ZdqjMNaZQMHIKfHz0J5APISHiMj+nUDGUyhj8kGeQKGdtLvbyZhAh/fw7g5hmEDfbnV31zqaQLnd6u7OBJlAcVRb1/Udm0A90cC7e0ibQAacoAlrVppAQsP3Fd+dmkDugEZEFJecQFDwu3agb6NAk/bFIzzRo0BNc2lmpoWjQIr38O7O1KBAkSRJkmS/nEBN1uzawX2cQFTEvbsbqZhALxJBRIRsmUCj6u3uTkaZQIaqt7sbo5dAHEA2M/NBl0BKEAk6XVCWQI4fEIo2C5RAyhOtgR7Pk0Ab1scURKaRQKvuCBFxU5FAvDhF3MOVk0Ci8N6VJniSQJfK2xHe8ZBAMzMzM7M+kUDOGQYAAOmRQLLHSDd01ZFA+YUTP2PYkUAvi3YCp1eRQAMjHKABupFArYh7d1dMkkAEV6itptaSQDxl2qMQgZNAQUgwM5MflEC22c/MrKmTQGEaH74iEJRAJsgdUDRxlkAcLPSXf7aXQEXV2929mJZAsI+SsKKqlkB4yrRHQdOWQLVIlWU5+ZZAF/P8/8/+lkA7N05VxcKWQOIA+n3fmpZA1osUm8c7l0C+PAjqIAuYQH1ZY2amo5hAaQDn2kGtmUA2ZshnqMSZQDarnK3m35lA5L/JzAzqm0AUrkfhAhWbQEXV292NK5tACVFUVfXom0COVYsKKT6cQIf38O6ON5xA",
+ "dtype": "f8"
+ }
+ },
+ {
+ "hoverinfo": "skip",
+ "marker": {
+ "color": "#1baf7a",
+ "line": {
+ "color": "#fcfcfb",
+ "width": 2
+ },
+ "size": 9
+ },
+ "mode": "markers+text",
+ "showlegend": false,
+ "text": [
+ " Sunflower oil"
+ ],
+ "textfont": {
+ "color": "#0b0b0b",
+ "size": 10
+ },
+ "textposition": "middle right",
+ "type": "scatter",
+ "x": [
+ "2026-06-01T00:00:00"
+ ],
+ "y": [
+ 1805.889583363636
+ ]
+ },
+ {
+ "hovertemplate": "Rapeseed oil
%{x|%b %Y} $%{y:.0f}",
+ "line": {
+ "color": "#eda100",
+ "width": 2
+ },
+ "mode": "lines",
+ "name": "Rapeseed oil",
+ "type": "scatter",
+ "x": [
+ "2015-01-01T00:00:00.000000",
+ "2015-02-01T00:00:00.000000",
+ "2015-03-01T00:00:00.000000",
+ "2015-04-01T00:00:00.000000",
+ "2015-05-01T00:00:00.000000",
+ "2015-06-01T00:00:00.000000",
+ "2015-07-01T00:00:00.000000",
+ "2015-08-01T00:00:00.000000",
+ "2015-09-01T00:00:00.000000",
+ "2015-10-01T00:00:00.000000",
+ "2015-11-01T00:00:00.000000",
+ "2015-12-01T00:00:00.000000",
+ "2016-01-01T00:00:00.000000",
+ "2016-02-01T00:00:00.000000",
+ "2016-03-01T00:00:00.000000",
+ "2016-04-01T00:00:00.000000",
+ "2016-05-01T00:00:00.000000",
+ "2016-06-01T00:00:00.000000",
+ "2016-07-01T00:00:00.000000",
+ "2016-08-01T00:00:00.000000",
+ "2016-09-01T00:00:00.000000",
+ "2016-10-01T00:00:00.000000",
+ "2016-11-01T00:00:00.000000",
+ "2016-12-01T00:00:00.000000",
+ "2017-01-01T00:00:00.000000",
+ "2017-02-01T00:00:00.000000",
+ "2017-03-01T00:00:00.000000",
+ "2017-04-01T00:00:00.000000",
+ "2017-05-01T00:00:00.000000",
+ "2017-06-01T00:00:00.000000",
+ "2017-07-01T00:00:00.000000",
+ "2017-08-01T00:00:00.000000",
+ "2017-09-01T00:00:00.000000",
+ "2017-10-01T00:00:00.000000",
+ "2017-11-01T00:00:00.000000",
+ "2017-12-01T00:00:00.000000",
+ "2018-01-01T00:00:00.000000",
+ "2018-02-01T00:00:00.000000",
+ "2018-03-01T00:00:00.000000",
+ "2018-04-01T00:00:00.000000",
+ "2018-05-01T00:00:00.000000",
+ "2018-06-01T00:00:00.000000",
+ "2018-07-01T00:00:00.000000",
+ "2018-08-01T00:00:00.000000",
+ "2018-09-01T00:00:00.000000",
+ "2018-10-01T00:00:00.000000",
+ "2018-11-01T00:00:00.000000",
+ "2018-12-01T00:00:00.000000",
+ "2019-01-01T00:00:00.000000",
+ "2019-02-01T00:00:00.000000",
+ "2019-03-01T00:00:00.000000",
+ "2019-04-01T00:00:00.000000",
+ "2019-05-01T00:00:00.000000",
+ "2019-06-01T00:00:00.000000",
+ "2019-07-01T00:00:00.000000",
+ "2019-08-01T00:00:00.000000",
+ "2019-09-01T00:00:00.000000",
+ "2019-10-01T00:00:00.000000",
+ "2019-11-01T00:00:00.000000",
+ "2019-12-01T00:00:00.000000",
+ "2020-01-01T00:00:00.000000",
+ "2020-02-01T00:00:00.000000",
+ "2020-03-01T00:00:00.000000",
+ "2020-04-01T00:00:00.000000",
+ "2020-05-01T00:00:00.000000",
+ "2020-06-01T00:00:00.000000",
+ "2020-07-01T00:00:00.000000",
+ "2020-08-01T00:00:00.000000",
+ "2020-09-01T00:00:00.000000",
+ "2020-10-01T00:00:00.000000",
+ "2020-11-01T00:00:00.000000",
+ "2020-12-01T00:00:00.000000",
+ "2021-01-01T00:00:00.000000",
+ "2021-02-01T00:00:00.000000",
+ "2021-03-01T00:00:00.000000",
+ "2021-04-01T00:00:00.000000",
+ "2021-05-01T00:00:00.000000",
+ "2021-06-01T00:00:00.000000",
+ "2021-07-01T00:00:00.000000",
+ "2021-08-01T00:00:00.000000",
+ "2021-09-01T00:00:00.000000",
+ "2021-10-01T00:00:00.000000",
+ "2021-11-01T00:00:00.000000",
+ "2021-12-01T00:00:00.000000",
+ "2022-01-01T00:00:00.000000",
+ "2022-02-01T00:00:00.000000",
+ "2022-03-01T00:00:00.000000",
+ "2022-04-01T00:00:00.000000",
+ "2022-05-01T00:00:00.000000",
+ "2022-06-01T00:00:00.000000",
+ "2022-07-01T00:00:00.000000",
+ "2022-08-01T00:00:00.000000",
+ "2022-09-01T00:00:00.000000",
+ "2022-10-01T00:00:00.000000",
+ "2022-11-01T00:00:00.000000",
+ "2022-12-01T00:00:00.000000",
+ "2023-01-01T00:00:00.000000",
+ "2023-02-01T00:00:00.000000",
+ "2023-03-01T00:00:00.000000",
+ "2023-04-01T00:00:00.000000",
+ "2023-05-01T00:00:00.000000",
+ "2023-06-01T00:00:00.000000",
+ "2023-07-01T00:00:00.000000",
+ "2023-08-01T00:00:00.000000",
+ "2023-09-01T00:00:00.000000",
+ "2023-10-01T00:00:00.000000",
+ "2023-11-01T00:00:00.000000",
+ "2023-12-01T00:00:00.000000",
+ "2024-01-01T00:00:00.000000",
+ "2024-02-01T00:00:00.000000",
+ "2024-03-01T00:00:00.000000",
+ "2024-04-01T00:00:00.000000",
+ "2024-05-01T00:00:00.000000",
+ "2024-06-01T00:00:00.000000",
+ "2024-07-01T00:00:00.000000",
+ "2024-08-01T00:00:00.000000",
+ "2024-09-01T00:00:00.000000",
+ "2024-10-01T00:00:00.000000",
+ "2024-11-01T00:00:00.000000",
+ "2024-12-01T00:00:00.000000",
+ "2025-01-01T00:00:00.000000",
+ "2025-02-01T00:00:00.000000",
+ "2025-03-01T00:00:00.000000",
+ "2025-04-01T00:00:00.000000",
+ "2025-05-01T00:00:00.000000",
+ "2025-06-01T00:00:00.000000",
+ "2025-07-01T00:00:00.000000",
+ "2025-08-01T00:00:00.000000",
+ "2025-09-01T00:00:00.000000",
+ "2025-10-01T00:00:00.000000",
+ "2025-11-01T00:00:00.000000",
+ "2025-12-01T00:00:00.000000",
+ "2026-01-01T00:00:00.000000",
+ "2026-02-01T00:00:00.000000",
+ "2026-03-01T00:00:00.000000",
+ "2026-04-01T00:00:00.000000",
+ "2026-05-01T00:00:00.000000",
+ "2026-06-01T00:00:00.000000"
+ ],
+ "y": {
+ "bdata": "wU5HH6gyiED2JRsPlluHQMXQCVZpO4dAzONcAU8yh0BBIdSdPd6HQKaWjoT5XYlAwBLQL7DjiECzMpxvRZOHQGtdADmW+YdASAvDvmcpiUAwOjPU+1KIQDrgR2RXVYlAAOACNzpPiEC+rHY6fVeIQPqnrYT/+odAzvlTEdYsiUAfdh0U9faIQBTIguExpohAldggfNTih0DC6Kk4CZeJQCyt/QcZhYpAEhQ/xhwjjEAQBejL1AKMQH9xyLu3qoxAtpKUXvyqjEAr3PKRpEKLQJiYd3JSP4pAzEHQ0Tq4iUB/uj2vBQ2KQKZ30g2A84lABzltiZsQi0BCdppzfVaLQPP6vXsv1otA+FSZVfYUjEDQNsBiLFeNQDjADbMUbItAKggZhKBUikDvqZz2tLGJQAQkBIzZ3IhAvgVAhCK7iEC2MvY/HkGJQFkWt7oIlYlAqSj0GcVUikCraKeiX6aKQCtQi8FjSYpAf3Py8+cxi0B1urXiWpaKQBoDba9Gm4lAcp0xsoY/ikDK669Cu5OJQB9HCKZuDYlAy5HByJUBiUAHkC5lGHSJQGMpkq+EAIpALkQ+EupFikAzC+CkM4yLQCIhIgtqLYxAdcazcrzOi0B0r+cAGCiMQJKzsKd90oxAgyf5VZJijUBoP1JEVtCLQGr7V1bayIhA6quMkbi6h0Dek4eF2sWIQPRklIZPgYpAI0p7g6/ai0CnpzfNaeeMQBKz1U9gTI1AbzW/I8D2jED/TlbLm1yQQMuFgRFv95BA0fzG7laMkUC28/3UeISTQAYv0LoKCpRALlhAwA8GlUAlBoGVQ7yYQKGe1LYW7ZhAf4WtJGWqlkBXJJDzSSWXQLouG1K5XZlAQZexI5Jvm0DE0uz4mWOcQGoxL+pOV5xAZMLqzP6XnECJ0t7g6w6bQGNRaqIg6KBAtcyN0lHloUAdoNwB6kGhQFxJSi8OGqBAY4YeB3dynUD3zXUvUQGaQNXs+JmbgZVABQe4YRaVlUBD5IqqVBuVQFJEDxKh/pJA7+XzXh6CkkCkGYumMxWSQBb365JK0Y9Ah+EjYkobj0B9HjA+xACMQFYOLbIdxo1AjUQBM/UYkUAi/fZ14CiQQPDiqbJYB5BAqFI8xF4WkUCTjBPJuDKQQI3WaI3Wlo5A/ePSEgknjUCNTRi6/wCNQEBp/FftDo9AoWZ0flQUkEAo8PVCcAyRQIKtEizeG5FARngyYto2kUApJW72EkKQQCjp9ql+UpBAPurJWjqZkUDwabUvyiOTQKJgp6PPDpJAdAu738uGkUDd71AUyJGRQObiw4GnPZJAhT6joFI2k0Czs1LpEueSQJmH+PLrxpNAN1WZcttjk0Ah9chNPP6SQBKlvcHX5pNA7Ex3o9N5lED7y+7Jk+qTQKg01WTOoZNASGvtyAQik0BQwkzbj+aTQOTzXj6PmZRAbqLCTybwlEDAi6fKogGXQA0dHDfI2JdA",
+ "dtype": "f8"
+ }
+ },
+ {
+ "hoverinfo": "skip",
+ "marker": {
+ "color": "#eda100",
+ "line": {
+ "color": "#fcfcfb",
+ "width": 2
+ },
+ "size": 9
+ },
+ "mode": "markers+text",
+ "showlegend": false,
+ "text": [
+ " Rapeseed oil"
+ ],
+ "textfont": {
+ "color": "#0b0b0b",
+ "size": 10
+ },
+ "textposition": "middle right",
+ "type": "scatter",
+ "x": [
+ "2026-06-01T00:00:00"
+ ],
+ "y": [
+ 1526.195522727273
+ ]
+ }
+ ],
+ "layout": {
+ "font": {
+ "color": "#52514e",
+ "size": 12
+ },
+ "height": 520,
+ "hovermode": "x unified",
+ "legend": {
+ "orientation": "h",
+ "x": 0,
+ "xanchor": "left",
+ "y": 1.02,
+ "yanchor": "bottom"
+ },
+ "margin": {
+ "b": 55,
+ "l": 70,
+ "r": 110,
+ "t": 70
+ },
+ "paper_bgcolor": "#fcfcfb",
+ "plot_bgcolor": "#fcfcfb",
+ "template": {
+ "data": {
+ "bar": [
+ {
+ "error_x": {
+ "color": "#2a3f5f"
+ },
+ "error_y": {
+ "color": "#2a3f5f"
+ },
+ "marker": {
+ "line": {
+ "color": "#E5ECF6",
+ "width": 0.5
+ },
+ "pattern": {
+ "fillmode": "overlay",
+ "size": 10,
+ "solidity": 0.2
+ }
+ },
+ "type": "bar"
+ }
+ ],
+ "barpolar": [
+ {
+ "marker": {
+ "line": {
+ "color": "#E5ECF6",
+ "width": 0.5
+ },
+ "pattern": {
+ "fillmode": "overlay",
+ "size": 10,
+ "solidity": 0.2
+ }
+ },
+ "type": "barpolar"
+ }
+ ],
+ "carpet": [
+ {
+ "aaxis": {
+ "endlinecolor": "#2a3f5f",
+ "gridcolor": "white",
+ "linecolor": "white",
+ "minorgridcolor": "white",
+ "startlinecolor": "#2a3f5f"
+ },
+ "baxis": {
+ "endlinecolor": "#2a3f5f",
+ "gridcolor": "white",
+ "linecolor": "white",
+ "minorgridcolor": "white",
+ "startlinecolor": "#2a3f5f"
+ },
+ "type": "carpet"
+ }
+ ],
+ "choropleth": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "type": "choropleth"
+ }
+ ],
+ "contour": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "colorscale": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "type": "contour"
+ }
+ ],
+ "contourcarpet": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "type": "contourcarpet"
+ }
+ ],
+ "heatmap": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "colorscale": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "type": "heatmap"
+ }
+ ],
+ "histogram": [
+ {
+ "marker": {
+ "pattern": {
+ "fillmode": "overlay",
+ "size": 10,
+ "solidity": 0.2
+ }
+ },
+ "type": "histogram"
+ }
+ ],
+ "histogram2d": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "colorscale": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "type": "histogram2d"
+ }
+ ],
+ "histogram2dcontour": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "colorscale": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "type": "histogram2dcontour"
+ }
+ ],
+ "mesh3d": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "type": "mesh3d"
+ }
+ ],
+ "parcoords": [
+ {
+ "line": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "parcoords"
+ }
+ ],
+ "pie": [
+ {
+ "automargin": true,
+ "type": "pie"
+ }
+ ],
+ "scatter": [
+ {
+ "fillpattern": {
+ "fillmode": "overlay",
+ "size": 10,
+ "solidity": 0.2
+ },
+ "type": "scatter"
+ }
+ ],
+ "scatter3d": [
+ {
+ "line": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scatter3d"
+ }
+ ],
+ "scattercarpet": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scattercarpet"
+ }
+ ],
+ "scattergeo": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scattergeo"
+ }
+ ],
+ "scattergl": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scattergl"
+ }
+ ],
+ "scattermap": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scattermap"
+ }
+ ],
+ "scattermapbox": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scattermapbox"
+ }
+ ],
+ "scatterpolar": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scatterpolar"
+ }
+ ],
+ "scatterpolargl": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scatterpolargl"
+ }
+ ],
+ "scatterternary": [
+ {
+ "marker": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "type": "scatterternary"
+ }
+ ],
+ "surface": [
+ {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ },
+ "colorscale": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "type": "surface"
+ }
+ ],
+ "table": [
+ {
+ "cells": {
+ "fill": {
+ "color": "#EBF0F8"
+ },
+ "line": {
+ "color": "white"
+ }
+ },
+ "header": {
+ "fill": {
+ "color": "#C8D4E3"
+ },
+ "line": {
+ "color": "white"
+ }
+ },
+ "type": "table"
+ }
+ ]
+ },
+ "layout": {
+ "annotationdefaults": {
+ "arrowcolor": "#2a3f5f",
+ "arrowhead": 0,
+ "arrowwidth": 1
+ },
+ "autotypenumbers": "strict",
+ "coloraxis": {
+ "colorbar": {
+ "outlinewidth": 0,
+ "ticks": ""
+ }
+ },
+ "colorscale": {
+ "diverging": [
+ [
+ 0,
+ "#8e0152"
+ ],
+ [
+ 0.1,
+ "#c51b7d"
+ ],
+ [
+ 0.2,
+ "#de77ae"
+ ],
+ [
+ 0.3,
+ "#f1b6da"
+ ],
+ [
+ 0.4,
+ "#fde0ef"
+ ],
+ [
+ 0.5,
+ "#f7f7f7"
+ ],
+ [
+ 0.6,
+ "#e6f5d0"
+ ],
+ [
+ 0.7,
+ "#b8e186"
+ ],
+ [
+ 0.8,
+ "#7fbc41"
+ ],
+ [
+ 0.9,
+ "#4d9221"
+ ],
+ [
+ 1,
+ "#276419"
+ ]
+ ],
+ "sequential": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ],
+ "sequentialminus": [
+ [
+ 0,
+ "#0d0887"
+ ],
+ [
+ 0.1111111111111111,
+ "#46039f"
+ ],
+ [
+ 0.2222222222222222,
+ "#7201a8"
+ ],
+ [
+ 0.3333333333333333,
+ "#9c179e"
+ ],
+ [
+ 0.4444444444444444,
+ "#bd3786"
+ ],
+ [
+ 0.5555555555555556,
+ "#d8576b"
+ ],
+ [
+ 0.6666666666666666,
+ "#ed7953"
+ ],
+ [
+ 0.7777777777777778,
+ "#fb9f3a"
+ ],
+ [
+ 0.8888888888888888,
+ "#fdca26"
+ ],
+ [
+ 1,
+ "#f0f921"
+ ]
+ ]
+ },
+ "colorway": [
+ "#636efa",
+ "#EF553B",
+ "#00cc96",
+ "#ab63fa",
+ "#FFA15A",
+ "#19d3f3",
+ "#FF6692",
+ "#B6E880",
+ "#FF97FF",
+ "#FECB52"
+ ],
+ "font": {
+ "color": "#2a3f5f"
+ },
+ "geo": {
+ "bgcolor": "white",
+ "lakecolor": "white",
+ "landcolor": "#E5ECF6",
+ "showlakes": true,
+ "showland": true,
+ "subunitcolor": "white"
+ },
+ "hoverlabel": {
+ "align": "left"
+ },
+ "hovermode": "closest",
+ "mapbox": {
+ "style": "light"
+ },
+ "paper_bgcolor": "white",
+ "plot_bgcolor": "#E5ECF6",
+ "polar": {
+ "angularaxis": {
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": ""
+ },
+ "bgcolor": "#E5ECF6",
+ "radialaxis": {
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": ""
+ }
+ },
+ "scene": {
+ "xaxis": {
+ "backgroundcolor": "#E5ECF6",
+ "gridcolor": "white",
+ "gridwidth": 2,
+ "linecolor": "white",
+ "showbackground": true,
+ "ticks": "",
+ "zerolinecolor": "white"
+ },
+ "yaxis": {
+ "backgroundcolor": "#E5ECF6",
+ "gridcolor": "white",
+ "gridwidth": 2,
+ "linecolor": "white",
+ "showbackground": true,
+ "ticks": "",
+ "zerolinecolor": "white"
+ },
+ "zaxis": {
+ "backgroundcolor": "#E5ECF6",
+ "gridcolor": "white",
+ "gridwidth": 2,
+ "linecolor": "white",
+ "showbackground": true,
+ "ticks": "",
+ "zerolinecolor": "white"
+ }
+ },
+ "shapedefaults": {
+ "line": {
+ "color": "#2a3f5f"
+ }
+ },
+ "ternary": {
+ "aaxis": {
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": ""
+ },
+ "baxis": {
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": ""
+ },
+ "bgcolor": "#E5ECF6",
+ "caxis": {
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": ""
+ }
+ },
+ "title": {
+ "x": 0.05
+ },
+ "xaxis": {
+ "automargin": true,
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": "",
+ "title": {
+ "standoff": 15
+ },
+ "zerolinecolor": "white",
+ "zerolinewidth": 2
+ },
+ "yaxis": {
+ "automargin": true,
+ "gridcolor": "white",
+ "linecolor": "white",
+ "ticks": "",
+ "title": {
+ "standoff": 15
+ },
+ "zerolinecolor": "white",
+ "zerolinewidth": 2
+ }
+ }
+ },
+ "title": {
+ "font": {
+ "color": "#0b0b0b",
+ "size": 17
+ },
+ "text": "The IMF edible-oil complex"
+ },
+ "xaxis": {
+ "gridcolor": "#e6e5e1",
+ "linecolor": "#e6e5e1",
+ "showspikes": false,
+ "zeroline": false
+ },
+ "yaxis": {
+ "gridcolor": "#e6e5e1",
+ "linecolor": "#e6e5e1",
+ "showspikes": false,
+ "title": {
+ "text": "USD / metric ton"
+ },
+ "zeroline": false
+ }
+ }
+ }
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "from aieng.forecasting.data.adapters import FREDAdapter\n",
+ "\n",
+ "\n",
+ "FRED_CACHE = ROOT / \"data\" / \"fred\"\n",
+ "COMPLEX = {\"Soybean oil\": \"PSOILUSDM\", \"Sunflower oil\": \"PSUNOUSDM\", \"Rapeseed oil\": \"PROILUSDM\"}\n",
+ "\n",
+ "oils = {\"Palm oil\": prices}\n",
+ "for name, fred_id in COMPLEX.items():\n",
+ " oils[name] = FREDAdapter(fred_id, cache_dir=FRED_CACHE).fetch()\n",
+ "\n",
+ "plot_oil_complex(oils)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "f700bf7c",
+ "metadata": {
+ "execution": {
+ "iopub.execute_input": "2026-08-06T19:18:04.739731Z",
+ "iopub.status.busy": "2026-08-06T19:18:04.739442Z",
+ "iopub.status.idle": "2026-08-06T19:18:04.761306Z",
+ "shell.execute_reply": "2026-08-06T19:18:04.759063Z"
+ }
+ },
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " | \n",
+ " Palm oil | \n",
+ " Soybean oil | \n",
+ " Sunflower oil | \n",
+ " Rapeseed oil | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " | Palm oil | \n",
+ " 1.00 | \n",
+ " 0.54 | \n",
+ " 0.60 | \n",
+ " 0.44 | \n",
+ "
\n",
+ " \n",
+ " | Soybean oil | \n",
+ " 0.54 | \n",
+ " 1.00 | \n",
+ " 0.55 | \n",
+ " 0.55 | \n",
+ "
\n",
+ " \n",
+ " | Sunflower oil | \n",
+ " 0.60 | \n",
+ " 0.55 | \n",
+ " 1.00 | \n",
+ " 0.62 | \n",
+ "
\n",
+ " \n",
+ " | Rapeseed oil | \n",
+ " 0.44 | \n",
+ " 0.55 | \n",
+ " 0.62 | \n",
+ " 1.00 | \n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ " Palm oil Soybean oil Sunflower oil Rapeseed oil\n",
+ "Palm oil 1.00 0.54 0.60 0.44\n",
+ "Soybean oil 0.54 1.00 0.55 0.55\n",
+ "Sunflower oil 0.60 0.55 1.00 0.62\n",
+ "Rapeseed oil 0.44 0.55 0.62 1.00"
+ ]
+ },
+ "execution_count": 9,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# Correlation of monthly returns — does the complex actually co-move?\n",
+ "returns = pd.DataFrame(\n",
+ " {name: frame.set_index(\"timestamp\")[\"value\"].pct_change() for name, frame in oils.items()}\n",
+ ").dropna()\n",
+ "returns.loc[\"2015-01-01\":].corr().round(2)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "00c65b63",
+ "metadata": {},
+ "source": [
+ "---\n",
+ "## 7. Where this leaves us\n",
+ "\n",
+ "| Decision | Status |\n",
+ "|---|---|\n",
+ "| Target series | `PPOILUSDM` — palm oil, monthly, 414 observations |\n",
+ "| Leak safety | True publication dates attached; verified in `pko.data` |\n",
+ "| Horizons | 1–6 months, which is 3–8 months past the last known price |\n",
+ "| Cutoffs | 7 candidates in `DEFAULT_CUTOFFS` — check them in section 4 |\n",
+ "\n",
+ "**Next:** a naive `LastValuePredictor` baseline over these cutoffs, to establish the\n",
+ "floor every other model has to beat.\n",
+ "\n",
+ "**Still open:** FRED has no palm *kernel* oil series, so this use case forecasts palm\n",
+ "oil. See [`DATA.md`](DATA.md).\n"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": ".venv (3.12.3)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.12.3"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/implementations/pko/DATA.md b/implementations/pko/DATA.md
new file mode 100644
index 00000000..5595761a
--- /dev/null
+++ b/implementations/pko/DATA.md
@@ -0,0 +1,137 @@
+# PKO Data Survey — What FRED Actually Has
+
+Survey of FRED's palm and edible-oil coverage, run 2026-08-06 with
+`scripts/explore_fred_oils.py`. Reproduce with:
+
+```bash
+uv run python scripts/explore_fred_oils.py
+uv run python scripts/explore_fred_oils.py --lag PPOILUSDM
+```
+
+---
+
+## Headline: FRED has no palm kernel oil
+
+Searching FRED for **"palm kernel oil" returns zero series.** The term is not in
+the catalogue. The closest available is palm *oil*, which is a related but
+genuinely different commodity with a different price.
+
+**This needs a team decision before anyone builds on it** — see
+[Open decision](#open-decision) below.
+
+## Headline: everything on FRED is monthly
+
+The survey covered 9 search terms and found 66 unique series. **All 66 are
+monthly.** There are no daily or weekly edible-oil series on FRED.
+
+This breaks the original plan's assumption of a weekly price series matched to
+weekly GDELT aggregation. Horizons have to be in months.
+
+---
+
+## The usable series
+
+Of the 66 hits, only **4 are actual prices** in dollars per tonne. The other 62
+are Producer Price or Consumer Price *indices* — base-year-relative numbers, not
+prices, and not forecastable as dollars.
+
+All four come from the same IMF release (Primary Commodity Prices), so they
+share a calendar, a lag, and a leak-safety fix.
+
+| FRED ID | Commodity | Freq | Units | Coverage | Samples |
+|---|---|---|---|---|---|
+| `PPOILUSDM` | Palm oil | Monthly | USD/tonne | 1992-01 → 2026-06 | 414 |
+| `PSOILUSDM` | Soybean oil | Monthly | USD/tonne | 1992-01 → 2026-06 | 414 |
+| `PSUNOUSDM` | Sunflower oil | Monthly | USD/tonne | 1992-01 → 2026-06 | 414 |
+| `PROILUSDM` | Rapeseed oil | Monthly | USD/tonne | 1992-01 → 2026-06 | 414 |
+
+### Price ranges observed
+
+| FRED ID | Min | Max | Latest (2026-06) |
+|---|---|---|---|
+| `PPOILUSDM` | 185 | 1,653 | 1,109 |
+| `PSOILUSDM` | 321 | 1,839 | 1,581 |
+| `PSUNOUSDM` | 333 | 2,537 | 1,806 |
+| `PROILUSDM` | 315 | 2,291 | 1,526 |
+
+**Proposal:** `PPOILUSDM` as the forecast target; the other three as covariates.
+They are close substitutes, cost nothing extra to add, and inherit the same
+leak-safe release handling.
+
+---
+
+## Release dates and leakage
+
+FRED stamps each observation with the **start of its reference period** — the
+June 2026 average is stamped `2026-06-01`. It is not published until weeks
+later. June 2026 appeared on **2026-07-13**.
+
+The library's `FREDAdapter` assumes `released_at = timestamp`, which would tell
+the harness the June price was knowable on June 1 — **42 days early, at every
+origin.** `implementations/pko/data.py` fixes this by fetching each
+observation's true first-publication date from FRED's real-time archive.
+
+### Publication lag, measured
+
+| FRED ID | Median lag | 90th pct | Vintages | Archive starts | Obs with exact release date |
+|---|---|---|---|---|---|
+| `PPOILUSDM` | 10 days | 28 days | 90 | 2015-11-06 | 128 of 414 |
+| `PSOILUSDM` | 10 days | 28 days | 90 | 2015-11-06 | 128 of 414 |
+| `PSUNOUSDM` | 9 days | 27 days | 90 | 2015-11-06 | 128 of 414 |
+| `PROILUSDM` | 9 days | 27 days | 90 | 2015-11-06 | 128 of 414 |
+
+"Lag" is days from the **end of the reference month** to the publication date.
+
+The 286 observations before 2015-11 are absent from FRED's archive and fall back
+to `month end + 29 days`. They serve as warmup history only — keep every
+forecast origin after 2015-11 and the fallback never affects a score.
+
+### The release calendar is irregular
+
+The IMF announces **no future release dates** to FRED. Recent releases:
+
+| Release date | Gap since previous |
+|---|---|
+| 2025-06-26 | — |
+| 2025-07-14 | 18 days |
+| 2026-01-22 | **192 days** |
+| 2026-02-12 | 21 days |
+| 2026-03-24 | 40 days |
+| 2026-04-15 | 22 days |
+| 2026-06-05 | 51 days |
+| 2026-07-13 | 38 days |
+
+Two consequences for experiment design:
+
+1. **There is a publication blackout from mid-July 2025 to late January 2026.**
+ Any forecast cutoff in that window sees prices frozen at roughly mid-2025. A
+ "quiet" cutoff there is quiet because no data existed, not because the market
+ was calm. Avoid the window, or choose it deliberately as a stress case.
+
+2. **The information set varies by cutoff.** Sometimes last month's price is
+ available at an origin, sometimes it isn't — 2026-06-05 published April and
+ May together. Baselines must tolerate a ragged edge.
+
+---
+
+## Open decision
+
+FRED has no palm kernel oil. The options:
+
+| Option | Consequence |
+|---|---|
+| **Forecast palm oil** (`PPOILUSDM`) | Stay on FRED, Vector-verifiable. Rename the use case from PKO. |
+| **Keep palm kernel oil** | Needs a non-FRED source (World Bank Pink Sheet has it, monthly). Vector would have to verify a new source. |
+
+The baseline work is nearly identical either way, so it is not blocking — but
+the target should be settled before notebooks and specs are written against it.
+
+---
+
+## Status
+
+- [x] Find the right FRED series — done, with the caveat above
+- [x] Load the price data — `implementations/pko/data.py`, leak-safe
+- [ ] Pull news from GDELT
+- [ ] Build a simple baseline forecast
+- [ ] Build an agent forecast and compare
diff --git a/implementations/pko/README.md b/implementations/pko/README.md
new file mode 100644
index 00000000..8cb1db74
--- /dev/null
+++ b/implementations/pko/README.md
@@ -0,0 +1,19 @@
+# Palm Kernel Oil (PKO) Price Forecasting
+
+Forecasting the palm kernel oil price using price history plus news.
+
+**Status:** just started — nothing built yet.
+
+## Plan
+
+- **Price data** — from FRED (monthly). Need to confirm which series.
+- **News** — from GDELT, filtered so we never use articles published after the forecast date.
+- **Copy from** — [`../energy_oil_forecasting/`](../energy_oil_forecasting/), which does the same thing for crude oil.
+
+## TODO
+
+- [ ] Find the right FRED series for palm kernel oil
+- [ ] Load the price data
+- [ ] Pull news from GDELT
+- [ ] Build a simple baseline forecast
+- [ ] Build an agent forecast and compare
diff --git a/implementations/pko/__init__.py b/implementations/pko/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/implementations/pko/data.py b/implementations/pko/data.py
new file mode 100644
index 00000000..e0847c4b
--- /dev/null
+++ b/implementations/pko/data.py
@@ -0,0 +1,428 @@
+"""Data-service setup for the palm oil price forecasting experiment.
+
+:func:`build_palm_oil_service` registers the IMF global palm oil price series
+(FRED ``PPOILUSDM``) with **honest release dates**, so the forecast harness can
+never hand a predictor a price that had not been published yet.
+
+Why release dates need fixing
+-----------------------------
+FRED stamps a monthly observation with the *start* of its reference period: the
+June 2026 average is stamped ``2026-06-01``. It is not actually published until
+weeks later -- June 2026 appeared on 2026-07-13.
+
+:class:`~aieng.forecasting.data.adapters.FREDAdapter` approximates
+``released_at = timestamp``, which would tell the harness the June price was
+knowable on June 1 -- 42 days before it existed. Across a monthly backtest that
+is a leak at *every* origin, and it would flatter any predictor we score.
+
+:class:`~aieng.forecasting.data.cutoff.CutoffEnforcer` already does the right
+thing when a ``released_at`` column is present, so the fix is to supply one:
+fetch the true first-publication date of every observation from FRED's real-time
+archive, attach it, and register the corrected frame via
+:class:`~aieng.forecasting.data.features.StaticFrameAdapter`.
+
+The release dates come from the FRED API's ``output_type=4`` (initial releases
+only), where each observation's ``realtime_start`` is the date it first became
+public. See ``scripts/explore_fred_oils.py`` for the survey that measured these
+lags and chose the fallback constant below.
+
+Two caveats, both handled here:
+
+- FRED's archive for this series starts 2015-11-06; observations first published
+ before then are omitted from the response entirely. Those fall back to
+ ``period_end + FALLBACK_RELEASE_LAG_DAYS``. They are warmup history only --
+ keep every forecast origin after 2015-11 and the fallback never binds.
+- A few recorded "initial releases" are FRED batch backfills (2019-06-18
+ published 22 periods at once), which look like multi-year lags. We use them
+ verbatim anyway: a release date later than the real one hides data, which is
+ conservative, never leaky.
+
+**Prerequisite:** ``FRED_API_KEY`` in the repo-root ``.env``. A free key is
+available at https://fred.stlouisfed.org/docs/api/api_key.html.
+
+Usage
+-----
+::
+
+ from pko.data import build_palm_oil_service, PALM_OIL_SERIES_ID
+
+ svc = build_palm_oil_service()
+ ctx = svc.context(as_of=datetime(2026, 7, 1))
+ df = ctx.get_series(PALM_OIL_SERIES_ID) # June 2026 correctly absent
+"""
+
+from __future__ import annotations
+
+import json
+import os
+import urllib.parse
+import urllib.request
+from datetime import datetime, timezone
+from pathlib import Path
+from typing import Any
+
+import pandas as pd
+from aieng.forecasting.data import DataService, SeriesMetadata
+from aieng.forecasting.data.adapters import FREDAdapter
+from aieng.forecasting.data.adapters.yfinance import YFinanceDailyAdapter
+from aieng.forecasting.data.features import StaticFrameAdapter
+
+
+PALM_OIL_SERIES_ID = "palm_oil_price"
+"""Canonical series ID used by specs, notebooks, and predictors.
+
+Note this is palm *oil*, not palm *kernel* oil -- FRED carries no palm kernel
+oil series at all (``scripts/explore_fred_oils.py`` returns zero hits for it).
+If the team switches targets, change :data:`FRED_SERIES_ID` and this ID together.
+"""
+
+FRED_SERIES_ID = "PPOILUSDM"
+"""FRED id: IMF Global price of Palm Oil, monthly, USD per metric ton."""
+
+FALLBACK_RELEASE_LAG_DAYS = 29
+"""Assumed lag for observations older than FRED's real-time archive.
+
+The 90th percentile of genuine (non-backfill) publication lags measured over
+2015-11 to 2026-06, rounded up. Median lag is 10 days; this is deliberately
+conservative.
+"""
+
+DEFAULT_CACHE_DIR = Path("data/fred")
+"""Parquet cache directory, shared with :class:`FREDAdapter` and fetch scripts."""
+
+_FRED_API_BASE = "https://api.stlouisfed.org/fred"
+_FRED_MIN_REALTIME = "1776-07-04"
+_FRED_MAX_REALTIME = "9999-12-31"
+
+
+def naive_utc_now() -> datetime:
+ """Return the current UTC time as a timezone-naive :class:`datetime`.
+
+ :class:`~aieng.forecasting.data.service.DataService` and
+ :class:`~aieng.forecasting.data.cutoff.CutoffEnforcer` require naive
+ ``as_of`` values; tz-aware timestamps raise on comparison with cached
+ series timestamps.
+
+ Returns
+ -------
+ datetime
+ Current UTC time with ``tzinfo`` stripped.
+ """
+ return datetime.now(tz=timezone.utc).replace(tzinfo=None)
+
+
+def fetch_release_dates(
+ fred_series_id: str = FRED_SERIES_ID,
+ *,
+ cache_dir: Path | None = None,
+ refresh: bool = False,
+) -> pd.DataFrame:
+ """Fetch each observation's true first-publication date from FRED.
+
+ Uses the FRED API's ``output_type=4`` (initial releases only). Results are
+ cached to ``{cache_dir}/{fred_series_id}_release_dates.parquet`` so repeated
+ runs need no network access.
+
+ Parameters
+ ----------
+ fred_series_id : str
+ FRED series identifier, e.g. ``"PPOILUSDM"``.
+ cache_dir : Path or None
+ Parquet cache directory. Defaults to :data:`DEFAULT_CACHE_DIR`.
+ refresh : bool
+ Force a network fetch and overwrite the cache.
+
+ Returns
+ -------
+ pd.DataFrame
+ Columns ``timestamp`` (observation period start) and ``released_at``
+ (date first published), sorted ascending. Covers only the periods
+ present in FRED's real-time archive.
+
+ Raises
+ ------
+ RuntimeError
+ If no API key is available and the cache is empty.
+ """
+ resolved_dir = cache_dir if cache_dir is not None else DEFAULT_CACHE_DIR
+ cache_path = resolved_dir / f"{fred_series_id}_release_dates.parquet"
+
+ if cache_path.exists() and not refresh:
+ return pd.read_parquet(cache_path)
+
+ api_key = os.environ.get("FRED_API_KEY")
+ if not api_key or api_key == "your_fred_api_key":
+ raise RuntimeError(
+ f"FRED_API_KEY is required to fetch release dates for {fred_series_id} "
+ f"(no cache at {cache_path}). Add it to the repo-root .env -- not .env.example, "
+ "which is tracked by git."
+ )
+
+ query = urllib.parse.urlencode(
+ {
+ "series_id": fred_series_id,
+ "output_type": 4,
+ "realtime_start": _FRED_MIN_REALTIME,
+ "realtime_end": _FRED_MAX_REALTIME,
+ "api_key": api_key,
+ "file_type": "json",
+ }
+ )
+ with urllib.request.urlopen(f"{_FRED_API_BASE}/series/observations?{query}", timeout=60) as response: # noqa: S310
+ payload: dict[str, Any] = json.load(response)
+
+ rows = [
+ {"timestamp": pd.Timestamp(obs["date"]), "released_at": pd.Timestamp(obs["realtime_start"])}
+ for obs in payload.get("observations", [])
+ if obs.get("value") not in (None, ".")
+ ]
+ frame = pd.DataFrame(rows).sort_values("timestamp").reset_index(drop=True)
+
+ resolved_dir.mkdir(parents=True, exist_ok=True)
+ frame.to_parquet(cache_path, index=False)
+ return frame
+
+
+def attach_release_dates(
+ observations: pd.DataFrame,
+ release_dates: pd.DataFrame,
+ *,
+ fallback_lag_days: int = FALLBACK_RELEASE_LAG_DAYS,
+) -> pd.DataFrame:
+ """Overwrite ``released_at`` with true publication dates where known.
+
+ Observations absent from FRED's real-time archive (those first published
+ before the archive begins) fall back to ``period_end + fallback_lag_days``,
+ where ``period_end`` is the last day of the observation's month.
+
+ Parameters
+ ----------
+ observations : pd.DataFrame
+ Canonical frame from :class:`FREDAdapter` with ``timestamp`` and
+ ``value`` columns.
+ release_dates : pd.DataFrame
+ Frame from :func:`fetch_release_dates`.
+ fallback_lag_days : int
+ Days after period end to assume for archive-less observations.
+
+ Returns
+ -------
+ pd.DataFrame
+ Columns ``timestamp``, ``value``, ``released_at``, sorted ascending.
+ """
+ frame = observations.loc[:, ["timestamp", "value"]].copy()
+ frame["timestamp"] = pd.to_datetime(frame["timestamp"])
+
+ lookup = release_dates.copy()
+ lookup["timestamp"] = pd.to_datetime(lookup["timestamp"])
+ frame = frame.merge(lookup, on="timestamp", how="left")
+
+ period_end = frame["timestamp"] + pd.offsets.MonthEnd(0)
+ fallback = period_end + pd.Timedelta(days=fallback_lag_days)
+ frame["released_at"] = frame["released_at"].fillna(fallback)
+
+ return frame.sort_values("timestamp").reset_index(drop=True)
+
+
+def build_palm_oil_service(
+ cache_dir: Path | None = None,
+ *,
+ refresh: bool = False,
+) -> DataService:
+ """Return a :class:`DataService` with the palm oil price series registered.
+
+ The registered series carries a true ``released_at`` column, so
+ :class:`~aieng.forecasting.data.cutoff.CutoffEnforcer` withholds each
+ observation until the date FRED actually published it.
+
+ Parameters
+ ----------
+ cache_dir : Path or None
+ Parquet cache directory for both the observations and the release
+ dates. Defaults to :data:`DEFAULT_CACHE_DIR`, resolved relative to the
+ current working directory.
+ refresh : bool
+ Force fresh network fetches for both the observations and the release
+ dates, overwriting the caches.
+
+ Returns
+ -------
+ DataService
+ Ready to hand to :func:`~aieng.forecasting.evaluation.backtest.backtest`
+ or :func:`~aieng.forecasting.evaluation.eval.evaluate`.
+ """
+ resolved_dir = cache_dir if cache_dir is not None else DEFAULT_CACHE_DIR
+
+ observations = FREDAdapter(FRED_SERIES_ID, cache_dir=resolved_dir, refresh=refresh).fetch()
+ release_dates = fetch_release_dates(FRED_SERIES_ID, cache_dir=resolved_dir, refresh=refresh)
+ corrected = attach_release_dates(observations, release_dates)
+
+ svc = DataService()
+ svc.register(
+ PALM_OIL_SERIES_ID,
+ StaticFrameAdapter(corrected),
+ SeriesMetadata(
+ series_id=PALM_OIL_SERIES_ID,
+ description=(
+ "IMF global benchmark price of palm oil, monthly average "
+ "(FRED PPOILUSDM), with true FRED publication dates as released_at"
+ ),
+ source="FRED (IMF Primary Commodity Prices)",
+ units="USD per metric ton",
+ frequency="MS",
+ table_id=f"fred:{FRED_SERIES_ID}",
+ ),
+ )
+ return svc
+
+
+# ── Daily futures target (primary) ───────────────────────────────────────────
+#
+# The FRED service above is a monthly, publication-lagged view: a price is
+# stamped with the start of its reference month but not released for ~2 months,
+# and FRED twice stopped publishing for half a year at a stretch -- including
+# straight through the 2022 Indonesian export ban. That caps the newest usable
+# forecast origin at 2025-08 and rules out weekly news alignment entirely.
+#
+# The daily CME Crude Palm Oil settlement price has neither problem: the
+# exchange publishes it the same day, so ``timestamp`` *is* the release date and
+# no ``released_at`` correction is needed. It tracks the FRED series at 0.92
+# correlation on monthly returns, with the peak strictly at zero lag.
+#
+# Caveats, recorded here so they stay attached to the data:
+#
+# - The contract is thinly traded (volume is reported on ~10% of days). CME
+# cash-settles it against the Bursa Malaysia FCPO benchmark, so the daily
+# number is an exchange settlement reference rather than a traded price.
+# Prices still move on zero-volume days (mean 0.87%), so the series is live,
+# not stale -- but do not describe it as a liquid market price.
+# - Yahoo keeps no vintage archive, so we assume history is never revised. The
+# repo's WTI implementation makes the same assumption for ``CL=F``.
+# - Jan--Jun 2016 is missing from Yahoo's history. Start backtests at 2017.
+
+PALM_OIL_DAILY_SERIES_ID = "palm_oil_futures_daily"
+"""Daily CME Crude Palm Oil settlement price."""
+
+PALM_OIL_WEEKLY_SERIES_ID = "palm_oil_futures_weekly"
+"""Weekly (Friday-close) resampling of the daily series.
+
+Weekly is the frequency that matches GDELT news aggregation, and the one the
+backtest specs target.
+"""
+
+YAHOO_TICKER = "CPO=F"
+"""Yahoo Finance ticker: CME Crude Palm Oil futures, continuous front month."""
+
+YAHOO_CACHE_DIR = Path("data/yfinance")
+"""Default yfinance cache directory, shared with the repo's other use cases."""
+
+_YAHOO_HISTORY_START = "2004-01-01"
+
+#: Yahoo's history for this contract has a hole in the first half of 2016.
+#: Backtests should start after it; recorded here so the reason is not lost.
+YAHOO_HISTORY_GAP = ("2016-01", "2016-06")
+
+
+def to_weekly(daily: pd.DataFrame) -> pd.DataFrame:
+ """Resample a daily price frame to Friday-close weekly observations.
+
+ Resampling with ``W-FRI`` labels every bin on its Friday even when that
+ Friday was a holiday, so the weekly grid has no missing labels. The
+ evaluation harness resolves ground truth by exact timestamp match, so a
+ complete, regular grid is what makes weekly forecast dates resolvable.
+
+ Parameters
+ ----------
+ daily : pd.DataFrame
+ Frame with ``timestamp`` and ``value`` columns.
+
+ Returns
+ -------
+ pd.DataFrame
+ Columns ``timestamp``, ``value``, ``released_at``. ``released_at``
+ equals ``timestamp`` -- an exchange settlement is public the same day.
+ """
+ series = daily.set_index("timestamp")["value"].resample("W-FRI").last().dropna()
+ return pd.DataFrame(
+ {"timestamp": series.index, "value": series.to_numpy(), "released_at": series.index}
+ ).reset_index(drop=True)
+
+
+def build_palm_oil_futures_service(
+ cache_dir: Path | None = None,
+ *,
+ start: str = _YAHOO_HISTORY_START,
+) -> DataService:
+ """Return a :class:`DataService` with daily *and* weekly palm oil prices.
+
+ Registers :data:`PALM_OIL_DAILY_SERIES_ID` (business-daily) and
+ :data:`PALM_OIL_WEEKLY_SERIES_ID` (Friday close). Both carry
+ ``released_at == timestamp``, which is correct for an exchange settlement
+ price and means the cutoff enforcer needs no correction.
+
+ Parameters
+ ----------
+ cache_dir : Path or None
+ yfinance cache directory. Defaults to :data:`YAHOO_CACHE_DIR`.
+ start : str
+ Earliest date requested from Yahoo Finance.
+
+ Returns
+ -------
+ DataService
+ Ready for the backtest and evaluation harnesses.
+ """
+ resolved_dir = cache_dir if cache_dir is not None else YAHOO_CACHE_DIR
+ adapter = YFinanceDailyAdapter(ticker=YAHOO_TICKER, start=start, cache_dir=resolved_dir)
+ daily = adapter.fetch()[["timestamp", "value"]].copy()
+ daily["timestamp"] = pd.to_datetime(daily["timestamp"]).dt.normalize()
+ daily = daily.dropna(subset=["value"]).sort_values("timestamp").reset_index(drop=True)
+ daily["released_at"] = daily["timestamp"]
+
+ svc = DataService()
+ svc.register(
+ PALM_OIL_DAILY_SERIES_ID,
+ StaticFrameAdapter(daily),
+ SeriesMetadata(
+ series_id=PALM_OIL_DAILY_SERIES_ID,
+ description=(
+ "CME Crude Palm Oil futures daily settlement price, cash-settled against "
+ "the Bursa Malaysia FCPO benchmark (Yahoo Finance CPO=F)"
+ ),
+ source="yfinance",
+ units="USD per metric ton",
+ frequency="B",
+ table_id=f"yahoo:{YAHOO_TICKER}:close",
+ ),
+ )
+ svc.register(
+ PALM_OIL_WEEKLY_SERIES_ID,
+ StaticFrameAdapter(to_weekly(daily)),
+ SeriesMetadata(
+ series_id=PALM_OIL_WEEKLY_SERIES_ID,
+ description="CME Crude Palm Oil settlement price, Friday close (Yahoo Finance CPO=F, resampled)",
+ source="yfinance, derived",
+ units="USD per metric ton",
+ frequency="W-FRI",
+ table_id=f"yahoo:{YAHOO_TICKER}:close-w-fri",
+ ),
+ )
+ return svc
+
+
+__all__ = [
+ "DEFAULT_CACHE_DIR",
+ "FALLBACK_RELEASE_LAG_DAYS",
+ "FRED_SERIES_ID",
+ "PALM_OIL_DAILY_SERIES_ID",
+ "PALM_OIL_SERIES_ID",
+ "PALM_OIL_WEEKLY_SERIES_ID",
+ "YAHOO_CACHE_DIR",
+ "YAHOO_HISTORY_GAP",
+ "YAHOO_TICKER",
+ "attach_release_dates",
+ "build_palm_oil_futures_service",
+ "build_palm_oil_service",
+ "fetch_release_dates",
+ "naive_utc_now",
+]
diff --git a/implementations/pko/plots.py b/implementations/pko/plots.py
new file mode 100644
index 00000000..14c5ef22
--- /dev/null
+++ b/implementations/pko/plots.py
@@ -0,0 +1,506 @@
+"""Interactive Plotly charts for palm oil price exploration.
+
+Every chart here is built for *deciding things* — which cutoffs to forecast from,
+whether the covariate oils move with palm, and how much the publication lag
+actually costs you — rather than for decoration.
+
+All figures are interactive: click a legend entry to hide a series, drag to zoom,
+double-click to reset, and hover for a shared crosshair readout.
+
+The palette is the validated four-slot categorical set (blue / orange / aqua /
+yellow). Aqua and yellow fall below 3:1 contrast on a light surface, so every
+chart that uses them also carries direct end-of-line labels — identity is never
+carried by colour alone.
+
+Usage
+-----
+::
+
+ from pko.data import build_palm_oil_service, PALM_OIL_SERIES_ID
+ from pko.plots import plot_price_history, DEFAULT_CUTOFFS
+
+ svc = build_palm_oil_service()
+ prices = svc.get_series(PALM_OIL_SERIES_ID, as_of=datetime.now())
+ plot_price_history(prices, cutoffs=DEFAULT_CUTOFFS).show()
+"""
+
+from __future__ import annotations
+
+from dataclasses import dataclass
+from datetime import datetime
+
+import pandas as pd
+import plotly.graph_objects as go
+
+
+# ── Palette ──────────────────────────────────────────────────────────────────
+# Validated categorical slots 1-4 plus surfaces and ink, for light and dark.
+# Swapping a whole dict swaps the theme; no chart code references raw hex.
+
+LIGHT_THEME: dict[str, str] = {
+ "surface": "#fcfcfb",
+ "text": "#0b0b0b",
+ "muted": "#52514e",
+ "grid": "#e6e5e1",
+ "series_1": "#2a78d6", # blue — palm oil (the target)
+ "series_2": "#eb6834", # orange — soybean oil
+ "series_3": "#1baf7a", # aqua — sunflower oil
+ "series_4": "#eda100", # yellow — rapeseed oil
+ "up": "#2a78d6",
+ "down": "#e34948",
+ "event": "rgba(235, 104, 52, 0.13)",
+ "quiet": "rgba(42, 120, 214, 0.10)",
+ "blackout": "rgba(227, 73, 72, 0.10)",
+}
+
+DARK_THEME: dict[str, str] = {
+ "surface": "#1a1a19",
+ "text": "#ffffff",
+ "muted": "#c3c2b7",
+ "grid": "#383835",
+ "series_1": "#3987e5",
+ "series_2": "#d95926",
+ "series_3": "#199e70",
+ "series_4": "#c98500",
+ "up": "#3987e5",
+ "down": "#e66767",
+ "event": "rgba(217, 89, 38, 0.18)",
+ "quiet": "rgba(57, 135, 229, 0.15)",
+ "blackout": "rgba(230, 103, 103, 0.15)",
+}
+
+
+@dataclass(frozen=True)
+class Cutoff:
+ """One forecast origin under consideration.
+
+ Parameters
+ ----------
+ date : str
+ Month-start cutoff date, ``YYYY-MM-DD``.
+ kind : str
+ ``"event"``, ``"quiet"``, or ``"stress"`` -- drives the shading colour.
+ label : str
+ Short human-readable reason this cutoff was chosen.
+ """
+
+ date: str
+ kind: str
+ label: str
+
+ @property
+ def timestamp(self) -> pd.Timestamp:
+ """Return the cutoff as a :class:`pandas.Timestamp`."""
+ return pd.Timestamp(self.date)
+
+
+#: Candidate cutoffs from the volatility scan. All have a 2-month information
+#: gap and resolve fully at horizons 1-6. Override in the notebook to explore.
+DEFAULT_CUTOFFS: list[Cutoff] = [
+ Cutoff("2021-05-01", "event", "June 2021 crash, -16.6%"),
+ Cutoff("2022-01-01", "event", "Indonesia export ban, -29.4% over 6mo"),
+ Cutoff("2023-04-01", "event", "May 2023 correction, -10.7%"),
+ Cutoff("2024-09-01", "event", "Oct 2024 rally, +9.7%"),
+ Cutoff("2023-07-01", "quiet", "calmest window, max move 4.1%"),
+ Cutoff("2024-11-01", "quiet", "max move 8.7%"),
+ Cutoff("2025-08-01", "quiet", "max move 5.9%"),
+]
+
+#: Periods when FRED published no new palm oil prices, from the release-date
+#: analysis in ``scripts/explore_fred_oils.py``.
+BLACKOUT_PERIODS: list[tuple[str, str]] = [
+ ("2021-12-01", "2022-08-01"),
+ ("2025-07-01", "2026-01-01"),
+]
+
+_HORIZONS = 6
+
+
+def _theme(dark: bool) -> dict[str, str]:
+ """Return the colour dict for the requested mode."""
+ return DARK_THEME if dark else LIGHT_THEME
+
+
+def _style(fig: go.Figure, theme: dict[str, str], *, title: str, ylabel: str, height: int = 500) -> go.Figure:
+ """Apply shared layout: recessive grid, unified hover, legend, sane margins.
+
+ Parameters
+ ----------
+ fig : go.Figure
+ Figure to restyle in place.
+ theme : dict
+ Colour dict from :func:`_theme`.
+ title : str
+ Chart title.
+ ylabel : str
+ Y-axis label.
+ height : int
+ Figure height in pixels.
+
+ Returns
+ -------
+ go.Figure
+ The same figure, restyled.
+ """
+ fig.update_layout(
+ title={"text": title, "font": {"size": 17, "color": theme["text"]}},
+ paper_bgcolor=theme["surface"],
+ plot_bgcolor=theme["surface"],
+ font={"color": theme["muted"], "size": 12},
+ height=height,
+ margin={"l": 70, "r": 110, "t": 70, "b": 55},
+ hovermode="x unified",
+ legend={"orientation": "h", "yanchor": "bottom", "y": 1.02, "xanchor": "left", "x": 0},
+ )
+ axis = {"gridcolor": theme["grid"], "zeroline": False, "linecolor": theme["grid"], "showspikes": False}
+ fig.update_xaxes(**axis)
+ fig.update_yaxes(**axis, title=ylabel)
+ return fig
+
+
+def _add_blackouts(fig: go.Figure, theme: dict[str, str], *, annotate: bool = True) -> None:
+ """Shade the periods when FRED published nothing."""
+ for i, (start, end) in enumerate(BLACKOUT_PERIODS):
+ fig.add_vrect(
+ x0=start,
+ x1=end,
+ fillcolor=theme["blackout"],
+ line_width=0,
+ layer="below",
+ annotation_text="no data published" if annotate and i == 0 else None,
+ annotation_position="top left",
+ annotation_font_size=10,
+ )
+
+
+def plot_price_history(
+ prices: pd.DataFrame,
+ *,
+ cutoffs: list[Cutoff] | None = None,
+ start: str | None = "2015-01-01",
+ dark: bool = False,
+) -> go.Figure:
+ """Plot the palm oil price history with candidate cutoffs marked.
+
+ Parameters
+ ----------
+ prices : pd.DataFrame
+ Frame with ``timestamp`` and ``value`` columns.
+ cutoffs : list of Cutoff or None
+ Cutoffs to mark with vertical lines. ``None`` marks none.
+ start : str or None
+ Clip the chart to this start date. ``None`` shows full history.
+ dark : bool
+ Render for a dark surface.
+
+ Returns
+ -------
+ go.Figure
+ Interactive line chart with a range slider.
+ """
+ theme = _theme(dark)
+ df = prices.copy()
+ if start is not None:
+ df = df[df["timestamp"] >= start]
+
+ fig = go.Figure()
+ fig.add_trace(
+ go.Scatter(
+ x=df["timestamp"],
+ y=df["value"],
+ mode="lines",
+ name="Palm oil",
+ line={"color": theme["series_1"], "width": 2},
+ hovertemplate="%{x|%b %Y}
$%{y:.0f}/tonne",
+ )
+ )
+
+ _add_blackouts(fig, theme)
+
+ for cut in cutoffs or []:
+ fig.add_vline(
+ x=cut.timestamp,
+ line={"color": theme["muted"], "width": 1, "dash": "dot"},
+ annotation_text=f"{cut.date[:7]} ({cut.kind})",
+ annotation_position="top",
+ annotation_font_size=9,
+ )
+
+ _style(fig, theme, title="Palm oil price (FRED PPOILUSDM)", ylabel="USD / metric ton", height=520)
+ fig.update_xaxes(rangeslider={"visible": True, "thickness": 0.06})
+ return fig
+
+
+def plot_cutoff_windows(
+ prices: pd.DataFrame,
+ *,
+ cutoffs: list[Cutoff] | None = None,
+ horizons: int = _HORIZONS,
+ dark: bool = False,
+) -> go.Figure:
+ """Shade each cutoff's forecast window over the price line.
+
+ Lets you check by eye whether a cutoff labelled "event" really has a shock in
+ its forecast window, and whether a "quiet" one really is flat.
+
+ Parameters
+ ----------
+ prices : pd.DataFrame
+ Frame with ``timestamp`` and ``value`` columns.
+ cutoffs : list of Cutoff or None
+ Cutoffs to shade. Defaults to :data:`DEFAULT_CUTOFFS`.
+ horizons : int
+ Number of months each forecast window spans.
+ dark : bool
+ Render for a dark surface.
+
+ Returns
+ -------
+ go.Figure
+ Interactive chart with one shaded band per cutoff.
+ """
+ theme = _theme(dark)
+ picks = cutoffs if cutoffs is not None else DEFAULT_CUTOFFS
+ lo = min(c.timestamp for c in picks) - pd.offsets.MonthBegin(6)
+ hi = max(c.timestamp for c in picks) + pd.offsets.MonthBegin(horizons + 6)
+ df = prices[(prices["timestamp"] >= lo) & (prices["timestamp"] <= hi)]
+
+ fig = go.Figure()
+ for cut in picks:
+ end = cut.timestamp + pd.offsets.MonthBegin(horizons)
+ fig.add_vrect(
+ x0=cut.timestamp,
+ x1=end,
+ fillcolor=theme.get(cut.kind, theme["quiet"]),
+ line_width=0,
+ layer="below",
+ annotation_text=f"{cut.date[:7]}
{cut.kind}",
+ annotation_position="top left",
+ annotation_font_size=9,
+ )
+
+ fig.add_trace(
+ go.Scatter(
+ x=df["timestamp"],
+ y=df["value"],
+ mode="lines",
+ name="Palm oil",
+ line={"color": theme["series_1"], "width": 2},
+ hovertemplate="%{x|%b %Y}
$%{y:.0f}/tonne",
+ )
+ )
+
+ return _style(
+ fig,
+ theme,
+ title=f"Candidate cutoffs and their {horizons}-month forecast windows",
+ ylabel="USD / metric ton",
+ height=520,
+ )
+
+
+def plot_information_gap(
+ service: object,
+ series_id: str,
+ *,
+ cutoffs: list[Cutoff] | None = None,
+ horizons: int = _HORIZONS,
+ dark: bool = False,
+) -> go.Figure:
+ """Contrast what a forecaster could see at each cutoff with what happened.
+
+ Draws the full realised price as a faint reference, then overlays -- per
+ cutoff -- the truncated history that was actually published by that date.
+ The visible gap between the end of each overlay and its cutoff line is the
+ publication lag, made concrete.
+
+ Parameters
+ ----------
+ service : object
+ A ``DataService`` exposing ``get_series(series_id, as_of=...)``.
+ series_id : str
+ Registered series id to query.
+ cutoffs : list of Cutoff or None
+ Cutoffs to draw. Defaults to :data:`DEFAULT_CUTOFFS`.
+ horizons : int
+ Months of forecast window to show past each cutoff.
+ dark : bool
+ Render for a dark surface.
+
+ Returns
+ -------
+ go.Figure
+ Interactive chart; click legend entries to isolate one cutoff.
+ """
+ theme = _theme(dark)
+ picks = cutoffs if cutoffs is not None else DEFAULT_CUTOFFS
+ truth = service.get_series(series_id, as_of=datetime.now()) # type: ignore[attr-defined]
+
+ lo = min(c.timestamp for c in picks) - pd.offsets.MonthBegin(12)
+ hi = max(c.timestamp for c in picks) + pd.offsets.MonthBegin(horizons + 3)
+ shown = truth[(truth["timestamp"] >= lo) & (truth["timestamp"] <= hi)]
+
+ fig = go.Figure()
+ fig.add_trace(
+ go.Scatter(
+ x=shown["timestamp"],
+ y=shown["value"],
+ mode="lines",
+ name="What actually happened",
+ line={"color": theme["muted"], "width": 1.5, "dash": "dot"},
+ hovertemplate="%{x|%b %Y}
actual $%{y:.0f}",
+ )
+ )
+
+ slots = ["series_1", "series_2", "series_3", "series_4"]
+ for i, cut in enumerate(picks):
+ seen = service.get_series(series_id, as_of=cut.timestamp.to_pydatetime()) # type: ignore[attr-defined]
+ seen = seen[seen["timestamp"] >= lo]
+ if seen.empty:
+ continue
+ colour = theme[slots[i % len(slots)]]
+ last = seen.iloc[-1]
+ fig.add_trace(
+ go.Scatter(
+ x=seen["timestamp"],
+ y=seen["value"],
+ mode="lines",
+ name=f"{cut.date[:7]} ({cut.kind})",
+ line={"color": colour, "width": 2},
+ hovertemplate=f"as of {cut.date[:7]}
%{{x|%b %Y}} $%{{y:.0f}}",
+ )
+ )
+ # Direct label at the data edge — identity never rests on colour alone.
+ fig.add_trace(
+ go.Scatter(
+ x=[last["timestamp"]],
+ y=[last["value"]],
+ mode="markers+text",
+ text=[f" {cut.date[:7]}"],
+ textposition="middle right",
+ textfont={"size": 10, "color": theme["text"]},
+ marker={"size": 9, "color": colour, "line": {"color": theme["surface"], "width": 2}},
+ showlegend=False,
+ hovertemplate=f"newest price available at {cut.date[:7]}
$%{{y:.0f}}",
+ )
+ )
+
+ return _style(
+ fig,
+ theme,
+ title="What the model can see at each cutoff, vs what really happened",
+ ylabel="USD / metric ton",
+ height=560,
+ )
+
+
+def plot_monthly_changes(prices: pd.DataFrame, *, start: str = "2020-01-01", dark: bool = False) -> go.Figure:
+ """Plot month-over-month percentage change, coloured by direction.
+
+ Parameters
+ ----------
+ prices : pd.DataFrame
+ Frame with ``timestamp`` and ``value`` columns.
+ start : str
+ Clip the chart to this start date.
+ dark : bool
+ Render for a dark surface.
+
+ Returns
+ -------
+ go.Figure
+ Interactive diverging bar chart with blackout periods shaded.
+ """
+ theme = _theme(dark)
+ df = prices.copy()
+ df["pct"] = df["value"].pct_change() * 100
+ df = df[df["timestamp"] >= start].dropna(subset=["pct"])
+
+ fig = go.Figure()
+ fig.add_trace(
+ go.Bar(
+ x=df["timestamp"],
+ y=df["pct"],
+ marker={"color": [theme["up"] if v >= 0 else theme["down"] for v in df["pct"]]},
+ name="Monthly change",
+ hovertemplate="%{x|%b %Y}
%{y:+.1f}%",
+ showlegend=False,
+ )
+ )
+ _add_blackouts(fig, theme)
+ fig.add_hline(y=0, line={"color": theme["muted"], "width": 1})
+
+ return _style(fig, theme, title="Palm oil, month-over-month change", ylabel="% change", height=420)
+
+
+def plot_oil_complex(frames: dict[str, pd.DataFrame], *, start: str = "2015-01-01", dark: bool = False) -> go.Figure:
+ """Plot palm oil against the other IMF edible oils on one axis.
+
+ All four series share units (USD/tonne), so a single axis is correct -- never
+ a second y-axis. Each line carries a direct end label, which also satisfies
+ the contrast relief rule for the aqua and yellow slots.
+
+ Parameters
+ ----------
+ frames : dict
+ Mapping of display name to frame with ``timestamp`` and ``value``.
+ Insertion order drives colour-slot assignment, so pass palm oil first.
+ start : str
+ Clip the chart to this start date.
+ dark : bool
+ Render for a dark surface.
+
+ Returns
+ -------
+ go.Figure
+ Interactive chart; click a legend entry to hide that oil.
+ """
+ theme = _theme(dark)
+ slots = ["series_1", "series_2", "series_3", "series_4"]
+
+ fig = go.Figure()
+ for i, (name, frame) in enumerate(frames.items()):
+ df = frame[frame["timestamp"] >= start]
+ if df.empty:
+ continue
+ colour = theme[slots[i % len(slots)]]
+ fig.add_trace(
+ go.Scatter(
+ x=df["timestamp"],
+ y=df["value"],
+ mode="lines",
+ name=name,
+ line={"color": colour, "width": 2},
+ hovertemplate=f"{name}
%{{x|%b %Y}} $%{{y:.0f}}",
+ )
+ )
+ last = df.iloc[-1]
+ fig.add_trace(
+ go.Scatter(
+ x=[last["timestamp"]],
+ y=[last["value"]],
+ mode="markers+text",
+ text=[f" {name}"],
+ textposition="middle right",
+ textfont={"size": 10, "color": theme["text"]},
+ marker={"size": 9, "color": colour, "line": {"color": theme["surface"], "width": 2}},
+ showlegend=False,
+ hoverinfo="skip",
+ )
+ )
+
+ return _style(fig, theme, title="The IMF edible-oil complex", ylabel="USD / metric ton", height=520)
+
+
+__all__ = [
+ "BLACKOUT_PERIODS",
+ "DARK_THEME",
+ "DEFAULT_CUTOFFS",
+ "LIGHT_THEME",
+ "Cutoff",
+ "plot_cutoff_windows",
+ "plot_information_gap",
+ "plot_monthly_changes",
+ "plot_oil_complex",
+ "plot_price_history",
+]
diff --git a/scripts/explore_fred_oils.py b/scripts/explore_fred_oils.py
new file mode 100644
index 00000000..3da2644a
--- /dev/null
+++ b/scripts/explore_fred_oils.py
@@ -0,0 +1,443 @@
+"""Survey FRED for candidate palm / edible-oil price series and their publication lags.
+
+This is a *selection* tool, not a fetch script. It answers the two questions we
+need settled before committing to a forecasting target:
+
+1. **What does FRED actually carry?** Searches the FRED series catalogue for
+ palm and edible-oil price series and reports id, title, frequency, units,
+ history span, and last-update date for each unique hit. Frequency is the
+ decisive column — a monthly target and a weekly target imply very different
+ experiment designs.
+
+2. **When was each observation really published?** FRED timestamps an
+ observation with the *start* of its reference period (a June monthly average
+ is stamped ``2026-06-01``) but does not publish it until weeks later. The
+ repo's :class:`~aieng.forecasting.data.adapters.FREDAdapter` approximates
+ ``released_at = timestamp``, which would let a predictor see a value up to
+ ~6 weeks before it existed. ``--lag`` measures the true lag from FRED's
+ real-time archive so we can populate an honest ``released_at`` column and let
+ :class:`~aieng.forecasting.data.cutoff.CutoffEnforcer` do its job.
+
+Publication lag is measured with ``output_type=4`` (initial releases only), where
+each observation's ``realtime_start`` *is* the date that value first became
+public.
+
+.. warning::
+ FRED's real-time archive does not extend to the beginning of most series --
+ for ``PPOILUSDM`` it starts 2015-11-06. Observations first published before
+ that date are **omitted entirely** from the ``output_type=4`` response (they
+ are not stamped with a floor date), so they need a fallback rule. Keep
+ forecast origins inside the vintage-covered window and the recorded release
+ dates are exact where it matters.
+
+.. warning::
+ Some recorded "initial releases" are FRED **batch backfills**, not real
+ publications -- ``PPOILUSDM`` shows 2017-07 through 2017-12 all first
+ appearing on 2019-06-18, a ~2-year apparent lag that reflects an archive
+ reconstruction rather than the IMF publishing late. ``--lag`` detects these
+ batches and excludes them from the typical-lag statistics, since including
+ them would inflate any percentile-based rule. They are still safe to use as
+ ``released_at`` values -- a late recorded release is conservative, never leaky.
+
+**Prerequisite:** ``FRED_API_KEY`` in the repo-root ``.env`` (or the
+environment). Free key: https://fred.stlouisfed.org/docs/api/api_key.html
+
+Usage
+-----
+::
+
+ # Survey the default search terms
+ uv run python scripts/explore_fred_oils.py
+
+ # Widen or narrow the search
+ uv run python scripts/explore_fred_oils.py --search "palm oil" "coconut oil"
+ uv run python scripts/explore_fred_oils.py --all-frequencies
+
+ # Measure the true publication lag for chosen candidates
+ uv run python scripts/explore_fred_oils.py --lag PPOILUSDM
+ uv run python scripts/explore_fred_oils.py --lag PPOILUSDM PSOILUSDM
+"""
+
+from __future__ import annotations
+
+import argparse
+import json
+import os
+import sys
+import time
+import urllib.error
+import urllib.parse
+import urllib.request
+from pathlib import Path
+from typing import Any
+
+
+REPO_ROOT = Path(__file__).resolve().parents[1]
+sys.path.insert(0, str(REPO_ROOT))
+
+from dotenv import load_dotenv
+
+
+load_dotenv(REPO_ROOT / ".env", override=False)
+
+import pandas as pd
+
+
+FRED_API_BASE = "https://api.stlouisfed.org/fred"
+
+#: Search terms covering the palm complex plus the substitutes it trades against.
+#: Edible oils are close substitutes, so a liquid neighbour can serve as a
+#: covariate even when it is not the target.
+DEFAULT_SEARCH_TERMS: list[str] = [
+ "palm oil",
+ "palm kernel oil",
+ "vegetable oil",
+ "edible oil",
+ "soybean oil",
+ "coconut oil",
+ "sunflower oil",
+ "rapeseed oil",
+ "fats and oils",
+]
+
+#: Sort order for the frequency column — finer resolution first, since that is
+#: the constraint that decides whether weekly news aggregation is even possible.
+_FREQUENCY_RANK: dict[str, int] = {
+ "D": 0,
+ "W": 1,
+ "BW": 2,
+ "M": 3,
+ "Q": 4,
+ "SA": 5,
+ "A": 6,
+}
+
+#: Courtesy delay between API calls. FRED allows 120 requests/minute.
+_REQUEST_DELAY_SECONDS = 0.3
+
+#: Widest real-time window FRED accepts, used to span a series' entire vintage archive.
+_FRED_MIN_REALTIME = "1776-07-04"
+_FRED_MAX_REALTIME = "9999-12-31"
+
+
+def get_api_key() -> str:
+ """Return the FRED API key, or exit with an actionable message.
+
+ Returns
+ -------
+ str
+ The API key from the ``FRED_API_KEY`` environment variable.
+ """
+ key = os.environ.get("FRED_API_KEY")
+ if not key or key == "your_fred_api_key":
+ sys.exit(
+ "FRED_API_KEY is not set.\n"
+ " 1. Request a free key: https://fred.stlouisfed.org/docs/api/api_key.html\n"
+ " 2. Add it to the repo-root .env (which is gitignored -- never .env.example):\n"
+ " printf 'FRED_API_KEY=%s\\n' 'YOUR_KEY_HERE' > .env"
+ )
+ return key
+
+
+def fred_get(endpoint: str, api_key: str, **params: Any) -> dict[str, Any]:
+ """Call a FRED API endpoint and return the decoded JSON payload.
+
+ Parameters
+ ----------
+ endpoint : str
+ Path below the API base, e.g. ``"series/search"``.
+ api_key : str
+ FRED API key.
+ **params : Any
+ Additional query parameters.
+
+ Returns
+ -------
+ dict
+ Decoded JSON response.
+
+ Raises
+ ------
+ SystemExit
+ If FRED rejects the request (most commonly an invalid key).
+ """
+ query = urllib.parse.urlencode({**params, "api_key": api_key, "file_type": "json"})
+ url = f"{FRED_API_BASE}/{endpoint}?{query}"
+ try:
+ with urllib.request.urlopen(url, timeout=60) as response: # noqa: S310
+ payload: dict[str, Any] = json.load(response)
+ except urllib.error.HTTPError as exc:
+ detail = exc.read().decode(errors="replace")[:300]
+ sys.exit(f"FRED API error {exc.code} on {endpoint}: {detail}")
+ time.sleep(_REQUEST_DELAY_SECONDS)
+ return payload
+
+
+def verify_key(api_key: str) -> None:
+ """Print a one-line confirmation that the key works."""
+ payload = fred_get("series", api_key, series_id="PPOILUSDM")
+ series = payload["seriess"][0]
+ print(
+ f"FRED API key OK — reference series {series['id']} ({series['frequency']}), updated {series['last_updated']}\n"
+ )
+
+
+def search_series(api_key: str, terms: list[str], limit_per_term: int) -> pd.DataFrame:
+ """Search FRED for each term and return the deduplicated union of hits.
+
+ Parameters
+ ----------
+ api_key : str
+ FRED API key.
+ terms : list[str]
+ Free-text search terms.
+ limit_per_term : int
+ Maximum hits to request per term.
+
+ Returns
+ -------
+ pd.DataFrame
+ One row per unique series id, with a ``matched_terms`` column recording
+ which search terms surfaced it.
+ """
+ hits: dict[str, dict[str, Any]] = {}
+ for term in terms:
+ payload = fred_get(
+ "series/search",
+ api_key,
+ search_text=term,
+ limit=limit_per_term,
+ order_by="popularity",
+ sort_order="desc",
+ )
+ found = payload.get("seriess", [])
+ print(f" {term:<20} {len(found):>3} hits")
+ for series in found:
+ existing = hits.setdefault(series["id"], {**series, "matched_terms": []})
+ existing["matched_terms"].append(term)
+
+ if not hits:
+ return pd.DataFrame()
+
+ frame = pd.DataFrame(hits.values())
+ frame["matched_terms"] = frame["matched_terms"].apply(", ".join)
+ frame["freq_rank"] = frame["frequency_short"].map(_FREQUENCY_RANK).fillna(99)
+ return frame.sort_values(["freq_rank", "popularity"], ascending=[True, False]).reset_index(drop=True)
+
+
+def print_catalogue(frame: pd.DataFrame, *, all_frequencies: bool) -> None:
+ """Print the search results as a readable table, finest frequency first."""
+ if frame.empty:
+ print("\nNo series found.")
+ return
+
+ shown = frame if all_frequencies else frame[frame["freq_rank"] <= _FREQUENCY_RANK["M"]]
+ dropped = len(frame) - len(shown)
+
+ print(f"\n{'=' * 118}\nCANDIDATE SERIES ({len(shown)} shown, sorted by frequency then popularity)\n{'=' * 118}")
+ header = f"{'SERIES_ID':<18} {'FREQ':<6} {'START':<11} {'END':<11} {'POP':>4} TITLE / UNITS"
+ print(header)
+ print("-" * 118)
+ for _, row in shown.iterrows():
+ print(
+ f"{row['id']:<18} {str(row['frequency_short']):<6} "
+ f"{row['observation_start']:<11} {row['observation_end']:<11} "
+ f"{int(row['popularity']):>4} {row['title'][:70]}"
+ )
+ print(f"{'':<18} {'':<6} {'':<11} {'':<11} {'':>4} units: {row['units_short']}")
+
+ if dropped:
+ print(f"\n({dropped} quarterly/annual/unranked series hidden — pass --all-frequencies to see them)")
+
+ _print_frequency_summary(shown)
+
+
+def _print_frequency_summary(frame: pd.DataFrame) -> None:
+ """Print a frequency histogram and call out any sub-monthly series."""
+ print(f"\n{'-' * 118}\nFREQUENCY BREAKDOWN")
+ for freq, count in frame["frequency_short"].value_counts().items():
+ print(f" {freq:<6} {count:>3} series")
+
+ sub_monthly = frame[frame["freq_rank"] < _FREQUENCY_RANK["M"]]
+ if sub_monthly.empty:
+ print(
+ "\n >> No daily or weekly series in these results. If a sub-monthly target is\n"
+ " required, FRED is not the source for it and the experiment design needs\n"
+ " to assume a monthly target."
+ )
+ else:
+ print(f"\n >> {len(sub_monthly)} sub-monthly series found: {', '.join(sub_monthly['id'])}")
+
+
+def measure_publication_lag(api_key: str, series_id: str) -> None:
+ """Report the true publication lag for a series from FRED's real-time archive.
+
+ Uses ``output_type=4`` (initial release only), where each observation's
+ ``realtime_start`` is the date that value first became public. Observations
+ predating the series' earliest vintage carry that floor date instead of a
+ true release date and are excluded from the statistics.
+
+ Parameters
+ ----------
+ api_key : str
+ FRED API key.
+ series_id : str
+ FRED series identifier, e.g. ``"PPOILUSDM"``.
+ """
+ meta = fred_get("series", api_key, series_id=series_id)["seriess"][0]
+ vintages = fred_get("series/vintagedates", api_key, series_id=series_id).get("vintage_dates", [])
+ # output_type=4 returns initial releases only, but the real-time window must span the
+ # whole archive — FRED defaults it to today, which holds no vintage and 400s.
+ initial = fred_get(
+ "series/observations",
+ api_key,
+ series_id=series_id,
+ output_type=4,
+ realtime_start=_FRED_MIN_REALTIME,
+ realtime_end=_FRED_MAX_REALTIME,
+ ).get("observations", [])
+ current = fred_get("series/observations", api_key, series_id=series_id).get("observations", [])
+
+ print(f"\n{'=' * 118}\nPUBLICATION LAG — {series_id}: {meta['title']}\n{'=' * 118}")
+ print(f" frequency : {meta['frequency']} ({meta['frequency_short']})")
+ print(f" units : {meta['units']}")
+ print(f" observation span : {meta['observation_start']} -> {meta['observation_end']}")
+
+ if not vintages:
+ print(" vintages : none recorded — publication lag cannot be measured.")
+ return
+
+ print(f" vintages : {len(vintages)} recorded, {vintages[0]} -> {vintages[-1]}")
+
+ frame = _build_lag_frame(initial, meta["frequency_short"])
+ n_total = len([o for o in current if o.get("value") not in (None, ".")])
+ n_missing = n_total - len(frame)
+
+ print("\n Initial-release coverage:")
+ print(f" observations with a value : {n_total}")
+ print(
+ f" with a true release date : {len(frame)} ({frame['timestamp'].min().date()} -> "
+ f"{frame['timestamp'].max().date()})"
+ )
+ print(f" older than the archive : {n_missing} (omitted by FRED — need the fallback rule)")
+
+ batches = _detect_backfill_batches(frame)
+ clean = frame[~frame["released_at"].isin(batches.index)]
+
+ if not batches.empty:
+ print(f"\n Batch backfills excluded from the statistics ({len(batches)} dates, archive artifacts):")
+ for release_date, count in batches.items():
+ print(f" {release_date.date()} published {count} periods at once")
+
+ lag = clean["lag_days"]
+ print(f"\n Typical lag after period end, measured on {len(clean)} genuine releases:")
+ print(f" median : {lag.median():.0f} days")
+ print(f" mean / min / max: {lag.mean():.1f} / {lag.min():.0f} / {lag.max():.0f} days")
+ print(f" 90th percentile : {lag.quantile(0.9):.0f} days")
+
+ print("\n Most recent releases:")
+ for _, row in frame.tail(6).iterrows():
+ print(
+ f" period {row['timestamp'].date()} (ends {row['period_end'].date()})"
+ f" -> published {row['released_at'].date()} (+{row['lag_days']:.0f}d)"
+ )
+
+ _print_lag_recommendation(lag, frame["timestamp"].min(), n_missing)
+
+
+def _build_lag_frame(observations: list[dict[str, Any]], frequency_short: str) -> pd.DataFrame:
+ """Return a frame of timestamp, period end, release date, and lag in days."""
+ rows = [
+ {"timestamp": pd.Timestamp(obs["date"]), "released_at": pd.Timestamp(obs["realtime_start"])}
+ for obs in observations
+ if obs.get("value") not in (None, ".")
+ ]
+ frame = pd.DataFrame(rows)
+ period_offsets = {"M": pd.offsets.MonthEnd(0), "Q": pd.offsets.QuarterEnd(0), "A": pd.offsets.YearEnd(0)}
+ offset = period_offsets.get(frequency_short)
+ frame["period_end"] = frame["timestamp"] + offset if offset is not None else frame["timestamp"]
+ frame["lag_days"] = (frame["released_at"] - frame["period_end"]).dt.days
+ return frame
+
+
+def _detect_backfill_batches(frame: pd.DataFrame, min_periods: int = 4) -> pd.Series:
+ """Return release dates that published many periods at once, with their counts.
+
+ A genuine monthly release publishes one new period. A release date carrying
+ several periods is an archive backfill, and the resulting multi-hundred-day
+ "lags" would distort any percentile-based rule.
+
+ Parameters
+ ----------
+ frame : pd.DataFrame
+ Lag frame from :func:`_build_lag_frame`.
+ min_periods : int
+ Number of periods on one release date above which it counts as a batch.
+
+ Returns
+ -------
+ pd.Series
+ Release date -> period count, for batch dates only.
+ """
+ counts = frame["released_at"].value_counts().sort_index()
+ return counts[counts >= min_periods]
+
+
+def _print_lag_recommendation(lag: pd.Series, archive_start: pd.Timestamp, n_missing: int) -> None:
+ """Print the concrete released_at rule implied by the measured lag."""
+ fallback = int(lag.quantile(0.9)) + 1
+ print(
+ f"\n >> RECOMMENDED released_at RULE\n"
+ f" - periods from {archive_start.date()} onward: use the true realtime_start from\n"
+ f" output_type=4 verbatim, batch backfills included. A recorded release later than\n"
+ f" the real one is conservative — it hides data, never leaks it.\n"
+ f" - the {n_missing} periods before {archive_start.date()}: period_end + {fallback} days\n"
+ f" (90th-percentile genuine lag, rounded up). These are warmup history only —\n"
+ f" keep every forecast origin after {archive_start.date()} and this rule never binds.\n"
+ f" - register the corrected frame via StaticFrameAdapter so CutoffEnforcer sees released_at"
+ )
+
+
+def parse_args() -> argparse.Namespace:
+ """Parse command-line arguments."""
+ parser = argparse.ArgumentParser(
+ description="Survey FRED for palm/edible-oil price series and measure their publication lags.",
+ formatter_class=argparse.RawDescriptionHelpFormatter,
+ )
+ parser.add_argument(
+ "--search", nargs="+", metavar="TERM", default=None, help="Search terms (default: oil complex)."
+ )
+ parser.add_argument("--limit", type=int, default=25, help="Max hits per search term (default: 25).")
+ parser.add_argument("--all-frequencies", action="store_true", help="Include quarterly/annual series in the table.")
+ parser.add_argument("--lag", nargs="+", metavar="SERIES_ID", default=None, help="Measure publication lag instead.")
+ parser.add_argument("--csv", type=Path, default=None, help="Write the catalogue table to this CSV path.")
+ return parser.parse_args()
+
+
+def main() -> None:
+ """Run the survey or the lag measurement, depending on the flags."""
+ args = parse_args()
+ api_key = get_api_key()
+ verify_key(api_key)
+
+ if args.lag:
+ for series_id in args.lag:
+ measure_publication_lag(api_key, series_id)
+ return
+
+ terms = args.search or DEFAULT_SEARCH_TERMS
+ print(f"Searching FRED for {len(terms)} terms:")
+ frame = search_series(api_key, terms, args.limit)
+ print_catalogue(frame, all_frequencies=args.all_frequencies)
+
+ if args.csv is not None and not frame.empty:
+ args.csv.parent.mkdir(parents=True, exist_ok=True)
+ frame.to_csv(args.csv, index=False)
+ print(f"\nWrote {len(frame)} rows to {args.csv}")
+
+ print("\nNext: measure the publication lag for your shortlist, e.g.")
+ print(" uv run python scripts/explore_fred_oils.py --lag PPOILUSDM")
+
+
+if __name__ == "__main__":
+ main()