diff --git a/guides/03-customize-agent-strategy.md b/guides/03-customize-agent-strategy.md index c39a03cf..a1bd2926 100644 --- a/guides/03-customize-agent-strategy.md +++ b/guides/03-customize-agent-strategy.md @@ -12,7 +12,7 @@ The [architecture atlas §05](https://vectorinstitute.github.io/agentic-forecast One object defines what an agent *is*: [`AgentConfig`](../aieng-forecasting/aieng/forecasting/methods/agentic/agent_factory.py) — name, model, system instruction, capabilities (search / code execution / function tools), and skill directories. Two more objects define its *role in an experiment*: a **prompt builder** (serializes the task + cutoff-scoped data into the user payload) and an **output schema** (the structured forecast it must return). [`AgentPredictor`](../aieng-forecasting/aieng/forecasting/methods/agentic/predictor.py) marries the two, and its `predict(task, context)` slots straight into the guide-2 harness. -That split is deliberate and worth internalizing: **the same identity can play several roles** (energy's notebook 03 runs one agent config against trajectory, binary-shock, and scenario tasks), and **the same role can be played by different identities** (which is how you A/B two strategies fairly). +That split is deliberate and worth internalizing: **the same identity can play several roles** (energy's notebook 03 is the worked example: one visible `analyst_config`, then three editable user-payload **task specs** for trajectory, binary-shock, and scenario), and **the same role can be played by different identities** (which is how you A/B two strategies fairly). Your edit surface is the **starter agent** — `starter_agent/` plus `99_starter_agent.ipynb` — a small, hackable template built for exactly this, and every implementation (#1 sp500, #2 food price, #3 energy, #4 BoC rate decisions) ships its own copy. This guide's file links point at energy's; open the pair for the track you actually picked. The **analyst agent** ([`analyst_agent/agent.py`](../implementations/energy_oil_forecasting/analyst_agent/agent.py)) is the finished four-level example to study: `basic` (no tools) → `news` (search) → `code_exec` (search + sandbox + skills) → `tool` (search + a fixed AutoARIMA function tool). diff --git a/implementations/energy_oil_forecasting/03_one_agent_three_tasks.ipynb b/implementations/energy_oil_forecasting/03_one_agent_three_tasks.ipynb index be72e8c3..fb33f8ec 100644 --- a/implementations/energy_oil_forecasting/03_one_agent_three_tasks.ipynb +++ b/implementations/energy_oil_forecasting/03_one_agent_three_tasks.ipynb @@ -2,7 +2,7 @@ "cells": [ { "cell_type": "markdown", - "id": "7fb27b941602401d91542211134fc71a", + "id": "8bd724aa", "metadata": {}, "source": [ "# WTI Oil Price Forecasting — One Agent, Three Tasks\n", @@ -10,52 +10,617 @@ "> **Part 3 of 7.** This notebook builds on the agentic predictor introduced in\n", "> [`02_intro_agentic_predictor.ipynb`](02_intro_agentic_predictor.ipynb).\n", "\n", - "A single Analyst Agent — backed by bounded Google Search — answers three tasks\n", - "using **one system prompt** and **task-specific user payloads**:\n", + "**Identity vs role.** One Analyst Agent (system prompt + toolbelt) answers three\n", + "different questions. The identity is fixed; only the **task spec** in the user\n", + "payload changes:\n", "\n", "| Stream | Task | Output |\n", "|--------|------|--------|\n", - "| A | Trajectory | 5/10/21-day price forecasts |\n", - "| B | Binary shock | P(WTI +$5 in 5 days) |\n", - "| C | Scenario analysis | Top 3 expert scenarios for 60 days |\n" + "| 1 | Trajectory | 5/10/21-day price forecasts |\n", + "| 2 | Binary shock | P(WTI +$5 in 5 days) |\n", + "| 3 | Scenario analysis | Top 3 expert scenarios for 60 days |\n", + "\n", + "A **task spec** is the ask: the question, the rules, and the required JSON shape.\n", + "It is *not* the system prompt. Edit the identity strings once, then edit each\n", + "stream's task spec and re-run (keep `USE_CACHE = False` after edits).\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "ddef6549", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Loaded 63 Prophet trajectory rows from energy_prophet_trajectories.parquet\n", + "Loaded 126 Prophet trajectory rows from energy_shock_prophet_trajectories.parquet\n", + "Price history through 2026-08-25\n" + ] + } + ], + "source": [ + "import json\n", + "import warnings\n", + "\n", + "import numpy as np\n", + "import pandas as pd\n", + "from IPython.display import Markdown, display # noqa: A004\n", + "\n", + "\n", + "warnings.filterwarnings(\"ignore\")\n", + "\n", + "# ── Model selection ───────────────────────────────────────────────────────────\n", + "# Two project models: \"gemini-3.1-flash-lite-preview\" (lite/default) and\n", + "# \"gemini-3.5-flash\" (advanced). Lite is the default here; switch to advanced\n", + "# for higher-quality runs.\n", + "AGENT_MODEL = \"gemini-3.1-flash-lite-preview\"\n", + "\n", + "# ── Cache control ─────────────────────────────────────────────────────────────\n", + "# Set to False to force a full end-to-end agent run (ignores all cached results).\n", + "# Keep False if you edit the identity or any stream's task spec.\n", + "USE_CACHE = False\n", + "\n", + "from aieng.forecasting.evaluation.task import ForecastingTask\n", + "from aieng.forecasting.methods.agentic import (\n", + " AgentPredictor,\n", + " ContinuousAgentForecastOutput,\n", + " DiscreteAgentForecastOutput,\n", + ")\n", + "from energy_oil_forecasting.analysis import compute_brier_score, trajectory_mae_table\n", + "from energy_oil_forecasting.analyst_agent import build_wti_multitask_news_config\n", + "from energy_oil_forecasting.data import WTI_SERIES_ID, build_wti_service, naive_utc_now\n", + "from energy_oil_forecasting.paths import (\n", + " PROPHET_SHOCK_TRAJ_CACHE,\n", + " PROPHET_TRAJ_CACHE,\n", + " SCENARIO_CACHE,\n", + " SCENARIO_ORIGIN,\n", + " SHOCK_ANALYST_CACHE,\n", + " SHOCK_HORIZON,\n", + " SHOCK_ORIGINS,\n", + " SHOCK_THRESHOLD,\n", + " TRAJ_AGENT_CACHE,\n", + " TRAJECTORY_ORIGINS,\n", + ")\n", + "from energy_oil_forecasting.prophet_baseline import (\n", + " check_shock_outcome,\n", + " load_prophet_trajectories,\n", + " prophet_prob_shock,\n", + " wti_series_to_price_df,\n", + ")\n", + "from energy_oil_forecasting.tasks import (\n", + " ScenarioAgentForecastOutput,\n", + " WtiMultitaskPromptBuilder,\n", + ")\n", + "from energy_oil_forecasting.viz import (\n", + " conf_bar,\n", + " make_shock_comparison_chart,\n", + " make_trajectory_fan_chart,\n", + " prob_bar,\n", + " verdict_label,\n", + ")\n", + "\n", + "\n", + "data_service = build_wti_service()\n", + "ctx = data_service.context(as_of=naive_utc_now())\n", + "price_df = wti_series_to_price_df(ctx.get_series(WTI_SERIES_ID))\n", + "\n", + "prophet_traj_df = load_prophet_trajectories(price_df, TRAJECTORY_ORIGINS, PROPHET_TRAJ_CACHE)\n", + "prophet_shock_df = load_prophet_trajectories(price_df, SHOCK_ORIGINS, PROPHET_SHOCK_TRAJ_CACHE)\n", + "print(f\"Price history through {price_df.index[-1].date()}\")\n", + "\n", + "\n", + "def preview_user_payload(builder: WtiMultitaskPromptBuilder, task: ForecastingTask, origin: pd.Timestamp) -> None:\n", + " \"\"\"Show the JSON user payload the agent would receive (no model call).\"\"\"\n", + " as_of = origin - pd.Timedelta(days=1)\n", + " origin_ctx = data_service.context(as_of=as_of)\n", + " payload = json.loads(builder(task=task, context=origin_ctx))\n", + " hist_lines = payload[\"target_history_csv\"].splitlines()\n", + " ask_prose, _, ask_schema = payload[\"task_spec\"].partition(\"Required JSON format:\")\n", + " display(\n", + " Markdown(\n", + " f\"### User payload preview \"\n", + " f\"*(as_of {payload['as_of']}, WTI ${payload['origin_price_usd_bbl']:.2f}/bbl)*\\n\\n\"\n", + " \"This is how we assign the task: the ask rides in `task_spec`; \"\n", + " \"horizons and quantiles come from the `ForecastingTask`.\\n\\n\"\n", + " f\"**Price history** — last 10 of {len(hist_lines) - 1} rows:\\n\\n\"\n", + " \"```\\n\" + \"\\n\".join(hist_lines[-10:]) + \"\\n```\\n\\n\"\n", + " f\"**horizons:** `{payload['horizons']}` · \"\n", + " f\"**standard_quantiles:** {len(payload['standard_quantiles'])} levels\\n\\n\"\n", + " f\"**task_spec** ({len(payload['task_spec'])} chars) — prose:\\n\\n\"\n", + " + ask_prose.strip()\n", + " + \"\\n\\n**Required JSON format:**\\n\\n```json\\n\"\n", + " + ask_schema.strip()\n", + " + \"\\n```\"\n", + " )\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "098f6650", + "metadata": {}, + "source": [ + "---\n", + "## Shared identity — system prompt + toolbelt\n", + "\n", + "This is what the agent *is*. The same `analyst_config` is reused by all three streams.\n", + "Edit the strings below to change persona or search behaviour; do **not** put the\n", + "trajectory / shock / scenario ask here — that belongs in each stream's task spec.\n" ] }, { "cell_type": "code", - "execution_count": null, - "id": "acae54e37e7d407bbb7b55eff062a284", + "execution_count": 2, + "id": "efdc7ca4", "metadata": {}, - "outputs": [], - "source": "import json\nimport warnings\n\nimport numpy as np\nimport pandas as pd\nfrom IPython.display import Markdown, display # noqa: A004\n\n\nwarnings.filterwarnings(\"ignore\")\n\n# ── Model selection ───────────────────────────────────────────────────────────\n# Two project models: \"gemini-3.1-flash-lite-preview\" (lite/default) and\n# \"gemini-3.5-flash\" (advanced). Lite is the default here; switch to advanced\n# for higher-quality runs.\nAGENT_MODEL = \"gemini-3.1-flash-lite-preview\"\n\n# ── Cache control ─────────────────────────────────────────────────────────────\n# Set to False to force a full end-to-end agent run (ignores all cached results).\nUSE_CACHE = False\n\nfrom aieng.forecasting.evaluation.task import ForecastingTask\nfrom energy_oil_forecasting.analysis import compute_brier_score, trajectory_mae_table\nfrom energy_oil_forecasting.data import WTI_SERIES_ID, build_wti_service, naive_utc_now\nfrom energy_oil_forecasting.paths import (\n PROPHET_SHOCK_TRAJ_CACHE,\n PROPHET_TRAJ_CACHE,\n SCENARIO_CACHE,\n SCENARIO_ORIGIN,\n SHOCK_ANALYST_CACHE,\n SHOCK_HORIZON,\n SHOCK_ORIGINS,\n SHOCK_THRESHOLD,\n TRAJ_AGENT_CACHE,\n TRAJECTORY_ORIGINS,\n)\nfrom energy_oil_forecasting.prophet_baseline import (\n check_shock_outcome,\n load_prophet_trajectories,\n prophet_prob_shock,\n wti_series_to_price_df,\n)\nfrom energy_oil_forecasting.tasks import TASK_SPECS, build_wti_news_predictor\nfrom energy_oil_forecasting.viz import (\n conf_bar,\n make_shock_comparison_chart,\n make_trajectory_fan_chart,\n prob_bar,\n verdict_label,\n)\n\n\ndata_service = build_wti_service()\nctx = data_service.context(as_of=naive_utc_now())\nprice_df = wti_series_to_price_df(ctx.get_series(WTI_SERIES_ID))\n\nprophet_traj_df = load_prophet_trajectories(price_df, TRAJECTORY_ORIGINS, PROPHET_TRAJ_CACHE)\nprophet_shock_df = load_prophet_trajectories(price_df, SHOCK_ORIGINS, PROPHET_SHOCK_TRAJ_CACHE)\nprint(f\"Price history through {price_df.index[-1].date()}\")" + "outputs": [ + { + "data": { + "text/markdown": [ + "### Toolbelt inventory *(agent `wti_analyst_multitask`, model `gemini-3.1-flash-lite-preview`)*\n", + "\n", + "| Capability | Status |\n", + "|---|---|\n", + "| `search_web` (context-retrieval sub-agent) | **on** — cutoff = payload `as_of` |\n", + "| Search model | `gemini-3.1-flash-lite-preview` |\n", + "| Temporal-leakage verifier | `gemini-3.5-flash` (max 3 attempts, confidence ≥ 8) |\n", + "| Skills | none (`skills_dirs` empty) |\n", + "| Code execution | **off** |\n", + "| `run_forecast` / function tools | **none** |\n", + "| `set_model_response` | attached **per stream** via `output_schema`, not by identity |\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "### System instruction\n", + "\n", + "```\n", + "## Role\n", + "\n", + "You are an expert WTI crude oil market analyst.\n", + "\n", + "## Input\n", + "\n", + "You will receive a JSON payload containing:\n", + "- `task_spec`: the exact question and required JSON output schema\n", + "- `as_of`: the forecast origin date (temporal cutoff)\n", + "- `horizons`: integer horizon steps (business days ahead)\n", + "- `standard_quantiles`: quantile levels for continuous forecasts (when applicable)\n", + "- `origin_price_usd_bbl`: WTI close on the origin date\n", + "- `target_history_csv`: compressed WTI daily close history\n", + "\n", + "When context retrieval is enabled, call ``search_web`` BEFORE answering.\n", + "\n", + "## Output contract\n", + "\n", + "Read the data (and briefing, if retrieved) carefully, then execute the task in `task_spec` precisely.\n", + "\n", + "If a `set_model_response` tool is available, call it with your complete JSON as `json_response` — the exact schema is described in `task_spec`. Otherwise return the JSON directly as plain text with no preamble.\n", + "```" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "### Search sub-agent instruction\n", + "\n", + "```\n", + "You are an oil market intelligence specialist with access to web search.\n", + "\n", + "Search for information relevant to the query and return a concise structured markdown summary (3-5 paragraphs) covering relevant aspects of:\n", + "- WTI/Brent crude price level and recent trend\n", + "- OPEC+ production decisions and supply outlook\n", + "- Geopolitical risks in the Persian Gulf, Middle East, key shipping lanes\n", + "- US Strategic Petroleum Reserve and energy policy signals\n", + "- Notable tanker/shipping incidents or supply disruption signals\n", + "- Published analyst forecasts or unusual price-target revisions\n", + "\n", + "Ground your summary in the search results you actually retrieve. When a cutoff date is specified, do not report or speculate about events that occurred after that date.\n", + "\n", + "Before finalizing your summary, reason step by step: (1) for each candidate fact, judge its actual recency from the substance of the result itself, never from a source's claimed publish date or byline timestamp — those are frequently stale or updated after original publication; (2) discard anything you cannot confidently place before the cutoff date; (3) only then write your summary. Do not supplement the search results with your own background/training knowledge — if the results are insufficient, say so explicitly rather than filling gaps from memory.\n", + "```" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# ── Editable identity (shared by Streams 1–3) ─────────────────────────────────\n", + "# Task-agnostic: persona + how to read the payload. The ask is NOT here.\n", + "\n", + "SYSTEM_INSTRUCTION = \"\"\"\n", + "## Role\n", + "\n", + "You are an expert WTI crude oil market analyst.\n", + "\n", + "## Input\n", + "\n", + "You will receive a JSON payload containing:\n", + "- `task_spec`: the exact question and required JSON output schema\n", + "- `as_of`: the forecast origin date (temporal cutoff)\n", + "- `horizons`: integer horizon steps (business days ahead)\n", + "- `standard_quantiles`: quantile levels for continuous forecasts (when applicable)\n", + "- `origin_price_usd_bbl`: WTI close on the origin date\n", + "- `target_history_csv`: compressed WTI daily close history\n", + "\n", + "When context retrieval is enabled, call ``search_web`` BEFORE answering.\n", + "\n", + "## Output contract\n", + "\n", + "Read the data (and briefing, if retrieved) carefully, then execute the task in `task_spec` precisely.\n", + "\n", + "If a `set_model_response` tool is available, call it with your complete JSON as `json_response` — the exact schema is described in `task_spec`. Otherwise return the JSON directly as plain text with no preamble.\n", + "\"\"\".strip()\n", + "\n", + "SEARCH_INSTRUCTION = \"\"\"\n", + "You are an oil market intelligence specialist with access to web search.\n", + "\n", + "Search for information relevant to the query and return a concise structured markdown summary (3-5 paragraphs) covering relevant aspects of:\n", + "- WTI/Brent crude price level and recent trend\n", + "- OPEC+ production decisions and supply outlook\n", + "- Geopolitical risks in the Persian Gulf, Middle East, key shipping lanes\n", + "- US Strategic Petroleum Reserve and energy policy signals\n", + "- Notable tanker/shipping incidents or supply disruption signals\n", + "- Published analyst forecasts or unusual price-target revisions\n", + "\n", + "Ground your summary in the search results you actually retrieve. When a cutoff date is specified, do not report or speculate about events that occurred after that date.\n", + "\n", + "Before finalizing your summary, reason step by step: (1) for each candidate fact, judge its actual recency from the substance of the result itself, never from a source's claimed publish date or byline timestamp — those are frequently stale or updated after original publication; (2) discard anything you cannot confidently place before the cutoff date; (3) only then write your summary. Do not supplement the search results with your own background/training knowledge — if the results are insufficient, say so explicitly rather than filling gaps from memory.\n", + "\"\"\".strip()\n", + "\n", + "_base = build_wti_multitask_news_config(model=AGENT_MODEL)\n", + "analyst_config = _base.model_copy(\n", + " update={\n", + " \"instruction\": SYSTEM_INSTRUCTION,\n", + " \"context_retrieval\": _base.context_retrieval.model_copy(update={\"instruction\": SEARCH_INSTRUCTION}),\n", + " }\n", + ")\n", + "\n", + "cr = analyst_config.context_retrieval\n", + "display(\n", + " Markdown(\n", + " f\"### Toolbelt inventory *(agent `{analyst_config.name}`, model `{analyst_config.model}`)*\\n\\n\"\n", + " \"| Capability | Status |\\n|---|---|\\n\"\n", + " f\"| `search_web` (context-retrieval sub-agent) | \"\n", + " f\"{'**on**' if cr.enabled else 'off'} — cutoff = payload `as_of` |\\n\"\n", + " f\"| Search model | `{cr.search_model}` |\\n\"\n", + " f\"| Temporal-leakage verifier | `{cr.verifier_model}` \"\n", + " f\"(max {cr.verifier_max_attempts} attempts, confidence ≥ {cr.verifier_confidence_threshold}) |\\n\"\n", + " f\"| Skills | none (`skills_dirs` empty) |\\n\"\n", + " f\"| Code execution | {'on' if analyst_config.code_execution.enabled else '**off**'} |\\n\"\n", + " f\"| `run_forecast` / function tools | \"\n", + " f\"{'yes' if analyst_config.function_tools else '**none**'} |\\n\"\n", + " \"| `set_model_response` | attached **per stream** via `output_schema`, not by identity |\\n\"\n", + " )\n", + ")\n", + "display(Markdown(\"### System instruction\\n\\n```\\n\" + SYSTEM_INSTRUCTION + \"\\n```\"))\n", + "display(Markdown(\"### Search sub-agent instruction\\n\\n```\\n\" + SEARCH_INSTRUCTION + \"\\n```\"))" + ] }, { "cell_type": "markdown", - "id": "9a63283cbaf04dbcab1f6479b197f3a8", + "id": "fc71e49f", "metadata": {}, "source": [ "---\n", "## Stream 1 — Trajectory Forecast\n", "\n", - "Compare Prophet fan charts to the news-grounded agent at three origins." + "**Question:** Where will WTI be in 5, 10, and 21 business days?\n", + "\n", + "Same identity as above. The task spec below is the ask — edit horizons or rules,\n", + "then re-run (keep `USE_CACHE = False`). Compare Prophet fan charts to the\n", + "news-grounded agent at three origins.\n", + "\n", + "**Try this:** set `TRAJECTORY_HORIZONS = [5, 21]` and update the spec wording to match.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "43f56708", + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "### Task spec — Stream 1\n", + "\n", + "Forecast the WTI crude oil price at each horizon listed in the payload\n", + "(`horizons`, business days ahead). Default horizons for this demo: [5, 10, 21].\n", + "\n", + "Rules:\n", + " - Produce one forecast for each horizon in `horizons`.\n", + " - Use exactly the quantile levels from `standard_quantiles` — no additions, no omissions.\n", + " - `point_forecast` must exactly equal the 0.50 quantile value.\n", + " - Quantile values must be strictly non-decreasing as quantile levels increase.\n", + " - Document your reasoning in the `rationale` fields.\n", + "\n", + "If a `set_model_response` tool is available, call it with your complete JSON as `json_response`. Otherwise return the JSON directly as plain text.\n", + "\n", + "**Required JSON format** (`ContinuousAgentForecastOutput`):\n", + "\n", + "```json\n", + "{\n", + " \"forecasts\": [\n", + " {\n", + " \"horizon\": \"\",\n", + " \"point_forecast\": \"\",\n", + " \"quantiles\": [\n", + " {\n", + " \"quantile\": 0.05,\n", + " \"value\": \"\"\n", + " },\n", + " {\n", + " \"quantile\": 0.1,\n", + " \"value\": \"\"\n", + " },\n", + " {\n", + " \"quantile\": 0.2,\n", + " \"value\": \"\"\n", + " },\n", + " {\n", + " \"quantile\": 0.3,\n", + " \"value\": \"\"\n", + " },\n", + " {\n", + " \"quantile\": 0.4,\n", + " \"value\": \"\"\n", + " },\n", + " {\n", + " \"quantile\": 0.5,\n", + " \"value\": \"\"\n", + " },\n", + " {\n", + " \"quantile\": 0.6,\n", + " \"value\": \"\"\n", + " },\n", + " {\n", + " \"quantile\": 0.7,\n", + " \"value\": \"\"\n", + " },\n", + " {\n", + " \"quantile\": 0.8,\n", + " \"value\": \"\"\n", + " },\n", + " {\n", + " \"quantile\": 0.9,\n", + " \"value\": \"\"\n", + " },\n", + " {\n", + " \"quantile\": 0.95,\n", + " \"value\": \"\"\n", + " }\n", + " ],\n", + " \"rationale\": \"\"\n", + " }\n", + " ],\n", + " \"rationale\": \"\"\n", + "}\n", + "```" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# ── Stream 1 task spec (edit this) ────────────────────────────────────────────\n", + "TRAJECTORY_HORIZONS = [5, 10, 21] # feeds ForecastingTask; listed again in the ask\n", + "\n", + "_TRAJ_SCHEMA = ContinuousAgentForecastOutput.prompt_schema_json()\n", + "TRAJECTORY_TASK_SPEC = f\"\"\"Forecast the WTI crude oil price at each horizon listed in the payload\n", + "(`horizons`, business days ahead). Default horizons for this demo: {TRAJECTORY_HORIZONS}.\n", + "\n", + "Rules:\n", + " - Produce one forecast for each horizon in `horizons`.\n", + " - Use exactly the quantile levels from `standard_quantiles` — no additions, no omissions.\n", + " - `point_forecast` must exactly equal the 0.50 quantile value.\n", + " - Quantile values must be strictly non-decreasing as quantile levels increase.\n", + " - Document your reasoning in the `rationale` fields.\n", + "\n", + "If a `set_model_response` tool is available, call it with your complete JSON as `json_response`. Otherwise return the JSON directly as plain text.\n", + "\n", + "Required JSON format:\n", + "{_TRAJ_SCHEMA}\n", + "\"\"\"\n", + "\n", + "_prose, _, _schema = TRAJECTORY_TASK_SPEC.partition(\"Required JSON format:\")\n", + "display(\n", + " Markdown(\n", + " \"### Task spec — Stream 1\\n\\n\"\n", + " + _prose.strip()\n", + " + \"\\n\\n**Required JSON format** (`ContinuousAgentForecastOutput`):\\n\\n```json\\n\"\n", + " + _schema.strip()\n", + " + \"\\n```\"\n", + " )\n", + ")" ] }, { "cell_type": "code", - "execution_count": null, - "id": "8dd0d8092fe74a7c96281538738b07e2", + "execution_count": 4, + "id": "6a01ba57", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Predictor schema: ContinuousAgentForecastOutput\n" + ] + }, + { + "data": { + "text/markdown": [ + "### User payload preview *(as_of 2026-03-01, WTI $65.21/bbl)*\n", + "\n", + "This is how we assign the task: the ask rides in `task_spec`; horizons and quantiles come from the `ForecastingTask`.\n", + "\n", + "**Price history** — last 10 of 1257 rows:\n", + "\n", + "```\n", + "2026-02-12,62.84\n", + "2026-02-13,62.89\n", + "2026-02-17,62.33\n", + "2026-02-18,65.19\n", + "2026-02-19,66.43\n", + "2026-02-20,66.39\n", + "2026-02-23,66.31\n", + "2026-02-24,65.63\n", + "2026-02-25,65.42\n", + "2026-02-26,65.21\n", + "```\n", + "\n", + "**horizons:** `[5, 10, 21]` · **standard_quantiles:** 11 levels\n", + "\n", + "**task_spec** (1832 chars) — prose:\n", + "\n", + "Forecast the WTI crude oil price at each horizon listed in the payload\n", + "(`horizons`, business days ahead). Default horizons for this demo: [5, 10, 21].\n", + "\n", + "Rules:\n", + " - Produce one forecast for each horizon in `horizons`.\n", + " - Use exactly the quantile levels from `standard_quantiles` — no additions, no omissions.\n", + " - `point_forecast` must exactly equal the 0.50 quantile value.\n", + " - Quantile values must be strictly non-decreasing as quantile levels increase.\n", + " - Document your reasoning in the `rationale` fields.\n", + "\n", + "If a `set_model_response` tool is available, call it with your complete JSON as `json_response`. Otherwise return the JSON directly as plain text.\n", + "\n", + "**Required JSON format:**\n", + "\n", + "```json\n", + "{\n", + " \"forecasts\": [\n", + " {\n", + " \"horizon\": \"\",\n", + " \"point_forecast\": \"\",\n", + " \"quantiles\": [\n", + " {\n", + " \"quantile\": 0.05,\n", + " \"value\": \"\"\n", + " },\n", + " {\n", + " \"quantile\": 0.1,\n", + " \"value\": \"\"\n", + " },\n", + " {\n", + " \"quantile\": 0.2,\n", + " \"value\": \"\"\n", + " },\n", + " {\n", + " \"quantile\": 0.3,\n", + " \"value\": \"\"\n", + " },\n", + " {\n", + " \"quantile\": 0.4,\n", + " \"value\": \"\"\n", + " },\n", + " {\n", + " \"quantile\": 0.5,\n", + " \"value\": \"\"\n", + " },\n", + " {\n", + " \"quantile\": 0.6,\n", + " \"value\": \"\"\n", + " },\n", + " {\n", + " \"quantile\": 0.7,\n", + " \"value\": \"\"\n", + " },\n", + " {\n", + " \"quantile\": 0.8,\n", + " \"value\": \"\"\n", + " },\n", + " {\n", + " \"quantile\": 0.9,\n", + " \"value\": \"\"\n", + " },\n", + " {\n", + " \"quantile\": 0.95,\n", + " \"value\": \"\"\n", + " }\n", + " ],\n", + " \"rationale\": \"\"\n", + " }\n", + " ],\n", + " \"rationale\": \"\"\n", + "}\n", + "```" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ + "# ── Assign role: wire identity + task spec (no model call) ────────────────────\n", "trajectory_task = ForecastingTask(\n", " task_id=\"wti_trajectory_demo\",\n", " target_series_id=WTI_SERIES_ID,\n", - " horizons=[5, 10, 21],\n", + " horizons=list(TRAJECTORY_HORIZONS),\n", " frequency=\"B\",\n", " description=\"Trajectory demo for NB3\",\n", ")\n", + "traj_prompt_builder = WtiMultitaskPromptBuilder(task_spec=TRAJECTORY_TASK_SPEC)\n", + "traj_predictor = AgentPredictor(\n", + " agent_config=analyst_config,\n", + " prompt_builder=traj_prompt_builder,\n", + " output_schema=ContinuousAgentForecastOutput,\n", + ")\n", "\n", - "traj_predictor = build_wti_news_predictor(\"trajectory\", model=AGENT_MODEL)\n", - "\n", + "print(f\"Predictor schema: {traj_predictor.output_schema.__name__}\")\n", + "preview_user_payload(traj_prompt_builder, trajectory_task, TRAJECTORY_ORIGINS[-1])" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "22ff64da", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saved 3 agent trajectory runs.\n", + "\n", + "Agent trajectory summary:\n", + " 2026-02-02 WTI=$62.14 h5=$65.2 | h10=$64.8 | h21=$64.0\n", + " 2026-02-23 WTI=$66.31 h5=$66.8 | h10=$67.2 | h21=$67.5\n", + " 2026-03-02 WTI=$71.23 h5=$65.4 | h10=$65.8 | h21=$66.3\n" + ] + } + ], + "source": [ + "# ── Run trajectory agent at three origins ─────────────────────────────────────\n", + "# Uses analyst_config + TRAJECTORY_TASK_SPEC. Keep USE_CACHE = False after edits.\n", "if USE_CACHE and TRAJ_AGENT_CACHE.exists():\n", " with open(TRAJ_AGENT_CACHE) as f:\n", " traj_agent_results = json.load(f)\n", @@ -76,11 +641,11 @@ " json.dump(traj_agent_results, f, indent=2)\n", " print(f\"Saved {len(traj_agent_results)} agent trajectory runs.\")\n", "\n", - "# Summary: agent point forecasts at each origin\n", "print(\"\\nAgent trajectory summary:\")\n", "for r in traj_agent_results:\n", " preds = r[\"predictions\"]\n", - " pts = [f\"h{[5, 10, 21][i]}=${preds[i]['payload']['point_forecast']:.1f}\" for i in range(len(preds))]\n", + " hs = TRAJECTORY_HORIZONS\n", + " pts = [f\"h{hs[i]}=${preds[i]['payload']['point_forecast']:.1f}\" for i in range(len(preds))]\n", " origin_price_rows = price_df[price_df.index >= pd.Timestamp(r[\"origin\"])]\n", " origin_price = f\"WTI=${origin_price_rows.iloc[0]['price']:.2f}\" if not origin_price_rows.empty else \"\"\n", " print(f\" {r['origin']} {origin_price} {' | '.join(pts)}\")" @@ -88,18 +653,2249 @@ }, { "cell_type": "code", - "execution_count": null, - "id": "024bf522", + "execution_count": 6, + "id": "01e5df63", "metadata": {}, - "outputs": [], - "source": "# ── I/O inspection: 2026-03-02 — conflict onset, most informative ────────────\nINSPECT_ORIGIN = \"2026-03-02\"\ninspect_rec = next((r for r in traj_agent_results if r[\"origin\"] == INSPECT_ORIGIN), None)\n\nif inspect_rec:\n origin_ts = pd.Timestamp(INSPECT_ORIGIN)\n bday_dates = pd.bdate_range(start=origin_ts + pd.offsets.BDay(1), periods=21)\n origin_price_row = price_df[price_df.index >= origin_ts]\n origin_price = float(origin_price_row.iloc[0][\"price\"]) if not origin_price_row.empty else float(\"nan\")\n\n preds = inspect_rec[\"predictions\"]\n rationale = preds[0].get(\"metadata\", {}).get(\"rationale\", \"\") if preds else \"\"\n\n table_rows = \"| Horizon | Agent ($) | 80% CI | Actual ($) | Agent err | Prophet err |\\n|---|---|---|---|---|---|\\n\"\n for i, h in enumerate([5, 10, 21]):\n actual_rows = price_df[price_df.index >= bday_dates[h - 1]]\n actual = float(actual_rows.iloc[0][\"price\"]) if not actual_rows.empty else float(\"nan\")\n pt = preds[i][\"payload\"][\"point_forecast\"]\n q10_val = next(\n (v for k, v in preds[i][\"payload\"][\"quantiles\"].items() if abs(float(k) - 0.1) < 1e-6), float(\"nan\")\n )\n q90_val = next(\n (v for k, v in preds[i][\"payload\"][\"quantiles\"].items() if abs(float(k) - 0.9) < 1e-6), float(\"nan\")\n )\n p_row = prophet_traj_df[(prophet_traj_df[\"origin\"] == origin_ts) & (prophet_traj_df[\"horizon\"] == h)]\n p_yhat = float(p_row.iloc[0][\"yhat\"]) if not p_row.empty else float(\"nan\")\n table_rows += (\n f\"| {h} bdays | **${pt:.1f}** | [{q10_val:.1f} – {q90_val:.1f}] \"\n f\"| ${actual:.1f} | {pt - actual:+.1f} | {p_yhat - actual:+.1f} |\\n\"\n )\n\n display(\n Markdown(\n f\"### Stream 1 — I/O Inspection: {INSPECT_ORIGIN} (WTI ${origin_price:.2f}/bbl)\\n\\n\"\n \"Agent and Prophet point forecasts vs realised prices at each horizon.\\n\\n\"\n + table_rows\n + (f\"\\n> **Agent rationale:** {rationale}\" if rationale else \"\")\n )\n )" + "outputs": [ + { + "data": { + "text/markdown": [ + "### Stream 1 — I/O Inspection: 2026-03-02 (WTI $71.23/bbl)\n", + "\n", + "Agent and Prophet point forecasts vs realised prices at each horizon.\n", + "\n", + "| Horizon | Agent ($) | 80% CI | Actual ($) | Agent err | Prophet err |\n", + "|---|---|---|---|---|---|\n", + "| 5 bdays | **$65.4** | [64.1 – 67.2] | $94.8 | -29.4 | -30.2 |\n", + "| 10 bdays | **$65.8** | [63.5 – 68.5] | $93.5 | -27.7 | -29.2 |\n", + "| 21 bdays | **$66.3** | [62.5 – 70.8] | $101.4 | -35.1 | -36.9 |\n", + "\n", + "> **Agent rationale:** The forecasts assume that WTI remains supported above the $64.00 range. Short-term momentum is slightly positive, leading to gradual appreciation across horizons. Quantiles are non-decreasing and broaden at further horizons to reflect cumulative volatility." + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# ── I/O inspection: 2026-03-02 — conflict onset, most informative ────────────\n", + "INSPECT_ORIGIN = \"2026-03-02\"\n", + "inspect_rec = next((r for r in traj_agent_results if r[\"origin\"] == INSPECT_ORIGIN), None)\n", + "\n", + "if inspect_rec:\n", + " origin_ts = pd.Timestamp(INSPECT_ORIGIN)\n", + " bday_dates = pd.bdate_range(start=origin_ts + pd.offsets.BDay(1), periods=max(TRAJECTORY_HORIZONS))\n", + " origin_price_row = price_df[price_df.index >= origin_ts]\n", + " origin_price = float(origin_price_row.iloc[0][\"price\"]) if not origin_price_row.empty else float(\"nan\")\n", + "\n", + " preds = inspect_rec[\"predictions\"]\n", + " rationale = preds[0].get(\"metadata\", {}).get(\"rationale\", \"\") if preds else \"\"\n", + "\n", + " table_rows = \"| Horizon | Agent ($) | 80% CI | Actual ($) | Agent err | Prophet err |\\n|---|---|---|---|---|---|\\n\"\n", + " for i, h in enumerate(TRAJECTORY_HORIZONS):\n", + " actual_rows = price_df[price_df.index >= bday_dates[h - 1]]\n", + " actual = float(actual_rows.iloc[0][\"price\"]) if not actual_rows.empty else float(\"nan\")\n", + " pt = preds[i][\"payload\"][\"point_forecast\"]\n", + " q10_val = next(\n", + " (v for k, v in preds[i][\"payload\"][\"quantiles\"].items() if abs(float(k) - 0.1) < 1e-6), float(\"nan\")\n", + " )\n", + " q90_val = next(\n", + " (v for k, v in preds[i][\"payload\"][\"quantiles\"].items() if abs(float(k) - 0.9) < 1e-6), float(\"nan\")\n", + " )\n", + " p_row = prophet_traj_df[(prophet_traj_df[\"origin\"] == origin_ts) & (prophet_traj_df[\"horizon\"] == h)]\n", + " p_yhat = float(p_row.iloc[0][\"yhat\"]) if not p_row.empty else float(\"nan\")\n", + " table_rows += (\n", + " f\"| {h} bdays | **${pt:.1f}** | [{q10_val:.1f} – {q90_val:.1f}] \"\n", + " f\"| ${actual:.1f} | {pt - actual:+.1f} | {p_yhat - actual:+.1f} |\\n\"\n", + " )\n", + "\n", + " display(\n", + " Markdown(\n", + " f\"### Stream 1 — I/O Inspection: {INSPECT_ORIGIN} (WTI ${origin_price:.2f}/bbl)\\n\\n\"\n", + " \"Agent and Prophet point forecasts vs realised prices at each horizon.\\n\\n\"\n", + " + table_rows\n", + " + (f\"\\n> **Agent rationale:** {rationale}\" if rationale else \"\")\n", + " )\n", + " )" + ] }, { "cell_type": "code", - "execution_count": null, - "id": "9e71dc29", + "execution_count": 7, + "id": "fa92b097", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.plotly.v1+json": { + "config": { + "plotlyServerURL": "https://plot.ly" + }, + "data": [ + { + "legendgroup": "actual", + "line": { + "color": "#bdd7e7", + "width": 1.5 + }, + "mode": "lines", + "name": "WTI Price", + "showlegend": true, + "type": "scatter", + "x": [ + "2025-12-04T00:00:00", + "2025-12-05T00:00:00", + "2025-12-08T00:00:00", + "2025-12-09T00:00:00", + "2025-12-10T00:00:00", + "2025-12-11T00:00:00", + "2025-12-12T00:00:00", + "2025-12-15T00:00:00", + "2025-12-16T00:00:00", + "2025-12-17T00:00:00", + "2025-12-18T00:00:00", + "2025-12-19T00:00:00", + "2025-12-22T00:00:00", + "2025-12-23T00:00:00", + "2025-12-24T00:00:00", + "2025-12-26T00:00:00", + "2025-12-29T00:00:00", + "2025-12-30T00:00:00", + "2025-12-31T00:00:00", + "2026-01-02T00:00:00", + "2026-01-05T00:00:00", + "2026-01-06T00:00:00", + "2026-01-07T00:00:00", + "2026-01-08T00:00:00", + "2026-01-09T00:00:00", + "2026-01-12T00:00:00", + "2026-01-13T00:00:00", + "2026-01-14T00:00:00", + "2026-01-15T00:00:00", + "2026-01-16T00:00:00", + "2026-01-20T00:00:00", + "2026-01-21T00:00:00", + "2026-01-22T00:00:00", + "2026-01-23T00:00:00", + "2026-01-26T00:00:00", + "2026-01-27T00:00:00", + "2026-01-28T00:00:00", + "2026-01-29T00:00:00", + "2026-01-30T00:00:00", + "2026-02-02T00:00:00" + ], + "xaxis": "x", + "y": [ + 59.66999816894531, + 60.08000183105469, + 58.880001068115234, + 58.25, + 58.459999084472656, + 57.599998474121094, + 57.439998626708984, + 56.81999969482422, + 55.27000045776367, + 55.939998626708984, + 56.150001525878906, + 56.65999984741211, + 58.0099983215332, + 58.380001068115234, + 58.349998474121094, + 56.7400016784668, + 58.08000183105469, + 57.95000076293945, + 57.41999816894531, + 57.31999969482422, + 58.31999969482422, + 57.130001068115234, + 55.9900016784668, + 57.7599983215332, + 59.119998931884766, + 59.5, + 61.150001525878906, + 62.02000045776367, + 59.189998626708984, + 59.439998626708984, + 60.34000015258789, + 60.619998931884766, + 59.36000061035156, + 61.06999969482422, + 60.630001068115234, + 62.38999938964844, + 63.209999084472656, + 65.41999816894531, + 65.20999908447266, + 62.13999938964844 + ], + "yaxis": "y" + }, + { + "legendgroup": "actual_outcome", + "line": { + "color": "#2171b5", + "width": 2.5 + }, + "mode": "lines", + "name": "Actual outcome", + "showlegend": true, + "type": "scatter", + "x": [ + "2026-02-03T00:00:00", + "2026-02-04T00:00:00", + "2026-02-05T00:00:00", + "2026-02-06T00:00:00", + "2026-02-09T00:00:00", + "2026-02-10T00:00:00", + "2026-02-11T00:00:00", + "2026-02-12T00:00:00", + "2026-02-13T00:00:00", + "2026-02-17T00:00:00", + "2026-02-18T00:00:00", + "2026-02-19T00:00:00", + "2026-02-20T00:00:00", + "2026-02-23T00:00:00", + "2026-02-24T00:00:00", + "2026-02-25T00:00:00", + "2026-02-26T00:00:00", + "2026-02-27T00:00:00", + "2026-03-02T00:00:00", + "2026-03-03T00:00:00" + ], + "xaxis": "x", + "y": [ + 63.209999084472656, + 65.13999938964844, + 63.290000915527344, + 63.54999923706055, + 64.36000061035156, + 63.959999084472656, + 64.62999725341797, + 62.84000015258789, + 62.88999938964844, + 62.33000183105469, + 65.19000244140625, + 66.43000030517578, + 66.38999938964844, + 66.30999755859375, + 65.62999725341797, + 65.41999816894531, + 65.20999908447266, + 67.0199966430664, + 71.2300033569336, + 74.55999755859375 + ], + "yaxis": "y" + }, + { + "fill": "toself", + "fillcolor": "rgba(99,99,99,0.12)", + "hoverinfo": "skip", + "line": { + "width": 0 + }, + "showlegend": false, + "type": "scatter", + "x": [ + "2026-02-03T00:00:00", + "2026-02-04T00:00:00", + "2026-02-05T00:00:00", + "2026-02-06T00:00:00", + "2026-02-09T00:00:00", + "2026-02-10T00:00:00", + "2026-02-11T00:00:00", + "2026-02-12T00:00:00", + "2026-02-13T00:00:00", + "2026-02-16T00:00:00", + "2026-02-17T00:00:00", + "2026-02-18T00:00:00", + "2026-02-19T00:00:00", + "2026-02-20T00:00:00", + "2026-02-23T00:00:00", + "2026-02-24T00:00:00", + "2026-02-25T00:00:00", + "2026-02-26T00:00:00", + "2026-02-27T00:00:00", + "2026-03-02T00:00:00", + "2026-03-03T00:00:00", + "2026-03-03T00:00:00", + "2026-03-02T00:00:00", + "2026-02-27T00:00:00", + "2026-02-26T00:00:00", + "2026-02-25T00:00:00", + "2026-02-24T00:00:00", + "2026-02-23T00:00:00", + "2026-02-20T00:00:00", + "2026-02-19T00:00:00", + "2026-02-18T00:00:00", + "2026-02-17T00:00:00", + "2026-02-16T00:00:00", + "2026-02-13T00:00:00", + "2026-02-12T00:00:00", + "2026-02-11T00:00:00", + "2026-02-10T00:00:00", + "2026-02-09T00:00:00", + "2026-02-06T00:00:00", + "2026-02-05T00:00:00", + "2026-02-04T00:00:00", + "2026-02-03T00:00:00" + ], + "xaxis": "x", + "y": [ + 43.65269745487135, + 45.11920693681949, + 44.33849920129854, + 44.84177811331387, + 45.65349369953454, + 46.11610260523638, + 44.03645427611295, + 44.72882967475964, + 45.510442323748734, + 46.53714225144238, + 46.53439771630021, + 46.27738521770241, + 46.337472355494874, + 46.34166262604237, + 46.22228637993722, + 47.216192211513366, + 46.978411738686326, + 47.17002234935417, + 47.52350099396734, + 48.53711086938469, + 46.63512127966007, + 82.38536439091916, + 84.69956537210625, + 83.52748182279198, + 81.42237423865565, + 82.09282228157007, + 82.7705384210447, + 82.90330652033943, + 82.52839263577388, + 80.96315150403905, + 82.3704475709958, + 82.91557652356165, + 81.0962535462679, + 80.79433428353565, + 81.89351931453137, + 79.88573094808363, + 80.76916263178083, + 78.89595549974204, + 79.91914430677082, + 79.94178027708254, + 80.614305228179, + 80.31882963193242 + ], + "yaxis": "y" + }, + { + "legendgroup": "prophet", + "line": { + "color": "#636363", + "dash": "dot", + "width": 1.8 + }, + "mode": "lines", + "name": "Prophet (95% CI)", + "showlegend": true, + "type": "scatter", + "x": [ + "2026-02-03T00:00:00", + "2026-02-04T00:00:00", + "2026-02-05T00:00:00", + "2026-02-06T00:00:00", + "2026-02-09T00:00:00", + "2026-02-10T00:00:00", + "2026-02-11T00:00:00", + "2026-02-12T00:00:00", + "2026-02-13T00:00:00", + "2026-02-16T00:00:00", + "2026-02-17T00:00:00", + "2026-02-18T00:00:00", + "2026-02-19T00:00:00", + "2026-02-20T00:00:00", + "2026-02-23T00:00:00", + "2026-02-24T00:00:00", + "2026-02-25T00:00:00", + "2026-02-26T00:00:00", + "2026-02-27T00:00:00", + "2026-03-02T00:00:00", + "2026-03-03T00:00:00" + ], + "xaxis": "x", + "y": [ + 62.34493252608951, + 62.3546260317816, + 62.37550928087007, + 62.40826961071988, + 62.581696959777766, + 62.66475471766877, + 62.759961550322785, + 62.86668446209037, + 62.984066404915026, + 63.3885326382581, + 63.536039644307586, + 63.68716464819795, + 63.840129855379566, + 63.99311040944458, + 64.43396591268382, + 64.56911296914909, + 64.69573660293823, + 64.81249374451193, + 64.91823479832485, + 65.1612279344997, + 65.21599854950432 + ], + "yaxis": "y" + }, + { + "error_y": { + "array": [ + 1.2999999999999972, + 2.200000000000003, + 3.799999999999997 + ], + "arrayminus": [ + 1.1000000000000083, + 1.7999999999999972, + 2.799999999999997 + ], + "color": "#2ca02c", + "symmetric": false, + "thickness": 2, + "type": "data", + "width": 6 + }, + "legendgroup": "agent", + "marker": { + "color": "#2ca02c", + "size": 11, + "symbol": "diamond" + }, + "mode": "markers", + "name": "Agent (80% CI)", + "showlegend": true, + "type": "scatter", + "x": [ + "2026-02-09T00:00:00", + "2026-02-16T00:00:00", + "2026-03-03T00:00:00" + ], + "xaxis": "x", + "y": [ + 65.2, + 64.8, + 64 + ], + "yaxis": "y" + }, + { + "legendgroup": "actual", + "line": { + "color": "#bdd7e7", + "width": 1.5 + }, + "mode": "lines", + "name": "WTI Price", + "showlegend": false, + "type": "scatter", + "x": [ + "2025-12-24T00:00:00", + "2025-12-26T00:00:00", + "2025-12-29T00:00:00", + "2025-12-30T00:00:00", + "2025-12-31T00:00:00", + "2026-01-02T00:00:00", + "2026-01-05T00:00:00", + "2026-01-06T00:00:00", + "2026-01-07T00:00:00", + "2026-01-08T00:00:00", + "2026-01-09T00:00:00", + "2026-01-12T00:00:00", + "2026-01-13T00:00:00", + "2026-01-14T00:00:00", + "2026-01-15T00:00:00", + "2026-01-16T00:00:00", + "2026-01-20T00:00:00", + "2026-01-21T00:00:00", + "2026-01-22T00:00:00", + "2026-01-23T00:00:00", + "2026-01-26T00:00:00", + "2026-01-27T00:00:00", + "2026-01-28T00:00:00", + "2026-01-29T00:00:00", + "2026-01-30T00:00:00", + "2026-02-02T00:00:00", + "2026-02-03T00:00:00", + "2026-02-04T00:00:00", + "2026-02-05T00:00:00", + "2026-02-06T00:00:00", + "2026-02-09T00:00:00", + "2026-02-10T00:00:00", + "2026-02-11T00:00:00", + "2026-02-12T00:00:00", + "2026-02-13T00:00:00", + "2026-02-17T00:00:00", + "2026-02-18T00:00:00", + "2026-02-19T00:00:00", + "2026-02-20T00:00:00", + "2026-02-23T00:00:00" + ], + "xaxis": "x2", + "y": [ + 58.349998474121094, + 56.7400016784668, + 58.08000183105469, + 57.95000076293945, + 57.41999816894531, + 57.31999969482422, + 58.31999969482422, + 57.130001068115234, + 55.9900016784668, + 57.7599983215332, + 59.119998931884766, + 59.5, + 61.150001525878906, + 62.02000045776367, + 59.189998626708984, + 59.439998626708984, + 60.34000015258789, + 60.619998931884766, + 59.36000061035156, + 61.06999969482422, + 60.630001068115234, + 62.38999938964844, + 63.209999084472656, + 65.41999816894531, + 65.20999908447266, + 62.13999938964844, + 63.209999084472656, + 65.13999938964844, + 63.290000915527344, + 63.54999923706055, + 64.36000061035156, + 63.959999084472656, + 64.62999725341797, + 62.84000015258789, + 62.88999938964844, + 62.33000183105469, + 65.19000244140625, + 66.43000030517578, + 66.38999938964844, + 66.30999755859375 + ], + "yaxis": "y2" + }, + { + "legendgroup": "actual_outcome", + "line": { + "color": "#2171b5", + "width": 2.5 + }, + "mode": "lines", + "name": "Actual outcome", + "showlegend": false, + "type": "scatter", + "x": [ + "2026-02-24T00:00:00", + "2026-02-25T00:00:00", + "2026-02-26T00:00:00", + "2026-02-27T00:00:00", + "2026-03-02T00:00:00", + "2026-03-03T00:00:00", + "2026-03-04T00:00:00", + "2026-03-05T00:00:00", + "2026-03-06T00:00:00", + "2026-03-09T00:00:00", + "2026-03-10T00:00:00", + "2026-03-11T00:00:00", + "2026-03-12T00:00:00", + "2026-03-13T00:00:00", + "2026-03-16T00:00:00", + "2026-03-17T00:00:00", + "2026-03-18T00:00:00", + "2026-03-19T00:00:00", + "2026-03-20T00:00:00", + "2026-03-23T00:00:00", + "2026-03-24T00:00:00" + ], + "xaxis": "x2", + "y": [ + 65.62999725341797, + 65.41999816894531, + 65.20999908447266, + 67.0199966430664, + 71.2300033569336, + 74.55999755859375, + 74.66000366210938, + 81.01000213623047, + 90.9000015258789, + 94.7699966430664, + 83.44999694824219, + 87.25, + 95.7300033569336, + 98.70999908447266, + 93.5, + 96.20999908447266, + 96.31999969482422, + 96.13999938964844, + 98.31999969482422, + 88.12999725341797, + 92.3499984741211 + ], + "yaxis": "y2" + }, + { + "fill": "toself", + "fillcolor": "rgba(99,99,99,0.12)", + "hoverinfo": "skip", + "line": { + "width": 0 + }, + "showlegend": false, + "type": "scatter", + "x": [ + "2026-02-24T00:00:00", + "2026-02-25T00:00:00", + "2026-02-26T00:00:00", + "2026-02-27T00:00:00", + "2026-03-02T00:00:00", + "2026-03-03T00:00:00", + "2026-03-04T00:00:00", + "2026-03-05T00:00:00", + "2026-03-06T00:00:00", + "2026-03-09T00:00:00", + "2026-03-10T00:00:00", + "2026-03-11T00:00:00", + "2026-03-12T00:00:00", + "2026-03-13T00:00:00", + "2026-03-16T00:00:00", + "2026-03-17T00:00:00", + "2026-03-18T00:00:00", + "2026-03-19T00:00:00", + "2026-03-20T00:00:00", + "2026-03-23T00:00:00", + "2026-03-24T00:00:00", + "2026-03-24T00:00:00", + "2026-03-23T00:00:00", + "2026-03-20T00:00:00", + "2026-03-19T00:00:00", + "2026-03-18T00:00:00", + "2026-03-17T00:00:00", + "2026-03-16T00:00:00", + "2026-03-13T00:00:00", + "2026-03-12T00:00:00", + "2026-03-11T00:00:00", + "2026-03-10T00:00:00", + "2026-03-09T00:00:00", + "2026-03-06T00:00:00", + "2026-03-05T00:00:00", + "2026-03-04T00:00:00", + "2026-03-03T00:00:00", + "2026-03-02T00:00:00", + "2026-02-27T00:00:00", + "2026-02-26T00:00:00", + "2026-02-25T00:00:00", + "2026-02-24T00:00:00" + ], + "xaxis": "x2", + "y": [ + 47.207344527997606, + 45.65123833213293, + 47.01162758886989, + 46.74075064586941, + 47.52100862702295, + 48.26734620676069, + 47.2912809202432, + 47.600431508135316, + 46.776516179186274, + 47.28473694839678, + 46.76252217684279, + 46.3527530922594, + 47.6946620056694, + 47.21799087039104, + 47.14020450218668, + 46.761381205485755, + 46.977549423995505, + 45.41048957069051, + 47.819060334186325, + 46.3054483998735, + 47.2144375359511, + 81.23392958070174, + 80.78207465857182, + 81.43478917616841, + 81.03062200126146, + 81.75240891068357, + 82.27671616914155, + 80.78645459127551, + 82.35556886919566, + 80.993627547953, + 81.62434412496637, + 81.98621680226682, + 82.51480068006585, + 82.3872614696908, + 81.70195819432526, + 81.36106835851712, + 83.21189540017585, + 81.90019614102818, + 81.37405245190956, + 80.98798516949226, + 81.361083301124, + 81.22707803539717 + ], + "yaxis": "y2" + }, + { + "legendgroup": "prophet", + "line": { + "color": "#636363", + "dash": "dot", + "width": 1.8 + }, + "mode": "lines", + "name": "Prophet (95% CI)", + "showlegend": false, + "type": "scatter", + "x": [ + "2026-02-24T00:00:00", + "2026-02-25T00:00:00", + "2026-02-26T00:00:00", + "2026-02-27T00:00:00", + "2026-03-02T00:00:00", + "2026-03-03T00:00:00", + "2026-03-04T00:00:00", + "2026-03-05T00:00:00", + "2026-03-06T00:00:00", + "2026-03-09T00:00:00", + "2026-03-10T00:00:00", + "2026-03-11T00:00:00", + "2026-03-12T00:00:00", + "2026-03-13T00:00:00", + "2026-03-16T00:00:00", + "2026-03-17T00:00:00", + "2026-03-18T00:00:00", + "2026-03-19T00:00:00", + "2026-03-20T00:00:00", + "2026-03-23T00:00:00", + "2026-03-24T00:00:00" + ], + "xaxis": "x2", + "y": [ + 64.0296674560543, + 64.15096466766626, + 64.26266162026774, + 64.36365678970431, + 64.59465106094737, + 64.64623868498259, + 64.68503086855034, + 64.71134386587318, + 64.72573089485647, + 64.70609096787761, + 64.68244936373456, + 64.65254467610664, + 64.61790086804672, + 64.58009639753811, + 64.46357677010484, + 64.42876847303489, + 64.39828559534017, + 64.37331804931387, + 64.35489047122242, + 64.34624222057208, + 64.36033923899178 + ], + "yaxis": "y2" + }, + { + "error_y": { + "array": [ + 2.3500000000000085, + 3.200000000000003, + 5 + ], + "arrayminus": [ + 2.049999999999997, + 3, + 4.700000000000003 + ], + "color": "#2ca02c", + "symmetric": false, + "thickness": 2, + "type": "data", + "width": 6 + }, + "legendgroup": "agent", + "marker": { + "color": "#2ca02c", + "size": 11, + "symbol": "diamond" + }, + "mode": "markers", + "name": "Agent (80% CI)", + "showlegend": false, + "type": "scatter", + "x": [ + "2026-03-02T00:00:00", + "2026-03-09T00:00:00", + "2026-03-24T00:00:00" + ], + "xaxis": "x2", + "y": [ + 66.85, + 67.2, + 67.5 + ], + "yaxis": "y2" + }, + { + "legendgroup": "actual", + "line": { + "color": "#bdd7e7", + "width": 1.5 + }, + "mode": "lines", + "name": "WTI Price", + "showlegend": false, + "type": "scatter", + "x": [ + "2026-01-02T00:00:00", + "2026-01-05T00:00:00", + "2026-01-06T00:00:00", + "2026-01-07T00:00:00", + "2026-01-08T00:00:00", + "2026-01-09T00:00:00", + "2026-01-12T00:00:00", + "2026-01-13T00:00:00", + "2026-01-14T00:00:00", + "2026-01-15T00:00:00", + "2026-01-16T00:00:00", + "2026-01-20T00:00:00", + "2026-01-21T00:00:00", + "2026-01-22T00:00:00", + "2026-01-23T00:00:00", + "2026-01-26T00:00:00", + "2026-01-27T00:00:00", + "2026-01-28T00:00:00", + "2026-01-29T00:00:00", + "2026-01-30T00:00:00", + "2026-02-02T00:00:00", + "2026-02-03T00:00:00", + "2026-02-04T00:00:00", + "2026-02-05T00:00:00", + "2026-02-06T00:00:00", + "2026-02-09T00:00:00", + "2026-02-10T00:00:00", + "2026-02-11T00:00:00", + "2026-02-12T00:00:00", + "2026-02-13T00:00:00", + "2026-02-17T00:00:00", + "2026-02-18T00:00:00", + "2026-02-19T00:00:00", + "2026-02-20T00:00:00", + "2026-02-23T00:00:00", + "2026-02-24T00:00:00", + "2026-02-25T00:00:00", + "2026-02-26T00:00:00", + "2026-02-27T00:00:00", + "2026-03-02T00:00:00" + ], + "xaxis": "x3", + "y": [ + 57.31999969482422, + 58.31999969482422, + 57.130001068115234, + 55.9900016784668, + 57.7599983215332, + 59.119998931884766, + 59.5, + 61.150001525878906, + 62.02000045776367, + 59.189998626708984, + 59.439998626708984, + 60.34000015258789, + 60.619998931884766, + 59.36000061035156, + 61.06999969482422, + 60.630001068115234, + 62.38999938964844, + 63.209999084472656, + 65.41999816894531, + 65.20999908447266, + 62.13999938964844, + 63.209999084472656, + 65.13999938964844, + 63.290000915527344, + 63.54999923706055, + 64.36000061035156, + 63.959999084472656, + 64.62999725341797, + 62.84000015258789, + 62.88999938964844, + 62.33000183105469, + 65.19000244140625, + 66.43000030517578, + 66.38999938964844, + 66.30999755859375, + 65.62999725341797, + 65.41999816894531, + 65.20999908447266, + 67.0199966430664, + 71.2300033569336 + ], + "yaxis": "y3" + }, + { + "legendgroup": "actual_outcome", + "line": { + "color": "#2171b5", + "width": 2.5 + }, + "mode": "lines", + "name": "Actual outcome", + "showlegend": false, + "type": "scatter", + "x": [ + "2026-03-03T00:00:00", + "2026-03-04T00:00:00", + "2026-03-05T00:00:00", + "2026-03-06T00:00:00", + "2026-03-09T00:00:00", + "2026-03-10T00:00:00", + "2026-03-11T00:00:00", + "2026-03-12T00:00:00", + "2026-03-13T00:00:00", + "2026-03-16T00:00:00", + "2026-03-17T00:00:00", + "2026-03-18T00:00:00", + "2026-03-19T00:00:00", + "2026-03-20T00:00:00", + "2026-03-23T00:00:00", + "2026-03-24T00:00:00", + "2026-03-25T00:00:00", + "2026-03-26T00:00:00", + "2026-03-27T00:00:00", + "2026-03-30T00:00:00", + "2026-03-31T00:00:00" + ], + "xaxis": "x3", + "y": [ + 74.55999755859375, + 74.66000366210938, + 81.01000213623047, + 90.9000015258789, + 94.7699966430664, + 83.44999694824219, + 87.25, + 95.7300033569336, + 98.70999908447266, + 93.5, + 96.20999908447266, + 96.31999969482422, + 96.13999938964844, + 98.31999969482422, + 88.12999725341797, + 92.3499984741211, + 90.31999969482422, + 94.4800033569336, + 99.63999938964844, + 102.87999725341795, + 101.37999725341795 + ], + "yaxis": "y3" + }, + { + "fill": "toself", + "fillcolor": "rgba(99,99,99,0.12)", + "hoverinfo": "skip", + "line": { + "width": 0 + }, + "showlegend": false, + "type": "scatter", + "x": [ + "2026-03-03T00:00:00", + "2026-03-04T00:00:00", + "2026-03-05T00:00:00", + "2026-03-06T00:00:00", + "2026-03-09T00:00:00", + "2026-03-10T00:00:00", + "2026-03-11T00:00:00", + "2026-03-12T00:00:00", + "2026-03-13T00:00:00", + "2026-03-16T00:00:00", + "2026-03-17T00:00:00", + "2026-03-18T00:00:00", + "2026-03-19T00:00:00", + "2026-03-20T00:00:00", + "2026-03-23T00:00:00", + "2026-03-24T00:00:00", + "2026-03-25T00:00:00", + "2026-03-26T00:00:00", + "2026-03-27T00:00:00", + "2026-03-30T00:00:00", + "2026-03-31T00:00:00", + "2026-03-31T00:00:00", + "2026-03-30T00:00:00", + "2026-03-27T00:00:00", + "2026-03-26T00:00:00", + "2026-03-25T00:00:00", + "2026-03-24T00:00:00", + "2026-03-23T00:00:00", + "2026-03-20T00:00:00", + "2026-03-19T00:00:00", + "2026-03-18T00:00:00", + "2026-03-17T00:00:00", + "2026-03-16T00:00:00", + "2026-03-13T00:00:00", + "2026-03-12T00:00:00", + "2026-03-11T00:00:00", + "2026-03-10T00:00:00", + "2026-03-09T00:00:00", + "2026-03-06T00:00:00", + "2026-03-05T00:00:00", + "2026-03-04T00:00:00", + "2026-03-03T00:00:00" + ], + "xaxis": "x3", + "y": [ + 46.22833196579188, + 48.5899508343703, + 47.1198125071567, + 46.40287493833531, + 47.57644055701692, + 47.40453033252249, + 47.35741176529119, + 47.18952563414713, + 47.80509993771512, + 45.64829915129681, + 46.93027362395508, + 46.764170172887695, + 47.87517645315977, + 48.170337791816216, + 47.953703748264765, + 47.35694727312822, + 47.15156340409221, + 47.62557254817828, + 46.68458175169467, + 47.41501071749509, + 47.169135097380575, + 80.72731253680165, + 81.4876157604624, + 82.44885862122469, + 82.025571787488, + 81.01078646130301, + 82.58438851514873, + 80.59825557081201, + 80.85313126810759, + 82.25348036249127, + 81.50208587124908, + 81.74745304990452, + 81.43275258106036, + 81.24781749416269, + 82.78728661279993, + 82.08233265003787, + 80.71126973826884, + 82.76839228265116, + 82.87366174540163, + 81.1347091302143, + 81.43860141078956, + 82.5522767039437 + ], + "yaxis": "y3" + }, + { + "legendgroup": "prophet", + "line": { + "color": "#636363", + "dash": "dot", + "width": 1.8 + }, + "mode": "lines", + "name": "Prophet (95% CI)", + "showlegend": false, + "type": "scatter", + "x": [ + "2026-03-03T00:00:00", + "2026-03-04T00:00:00", + "2026-03-05T00:00:00", + "2026-03-06T00:00:00", + "2026-03-09T00:00:00", + "2026-03-10T00:00:00", + "2026-03-11T00:00:00", + "2026-03-12T00:00:00", + "2026-03-13T00:00:00", + "2026-03-16T00:00:00", + "2026-03-17T00:00:00", + "2026-03-18T00:00:00", + "2026-03-19T00:00:00", + "2026-03-20T00:00:00", + "2026-03-23T00:00:00", + "2026-03-24T00:00:00", + "2026-03-25T00:00:00", + "2026-03-26T00:00:00", + "2026-03-27T00:00:00", + "2026-03-30T00:00:00", + "2026-03-31T00:00:00" + ], + "xaxis": "x3", + "y": [ + 64.55282972823227, + 64.58948681056167, + 64.61342817766575, + 64.62523437274105, + 64.59691316091426, + 64.57017958588762, + 64.53714308772618, + 64.49936269284693, + 64.45845053891476, + 64.33308327160422, + 64.29556931134806, + 64.26253573305412, + 64.23518610706591, + 64.21455434023738, + 64.2003885485236, + 64.213000244922, + 64.23445039564184, + 64.26452318491137, + 64.30280307301257, + 64.46008126343348, + 64.52365249625244 + ], + "yaxis": "y3" + }, + { + "error_y": { + "array": [ + 1.7999999999999972, + 2.700000000000003, + 4.5 + ], + "arrayminus": [ + 1.3000000000000114, + 2.299999999999997, + 3.799999999999997 + ], + "color": "#2ca02c", + "symmetric": false, + "thickness": 2, + "type": "data", + "width": 6 + }, + "legendgroup": "agent", + "marker": { + "color": "#2ca02c", + "size": 11, + "symbol": "diamond" + }, + "mode": "markers", + "name": "Agent (80% CI)", + "showlegend": false, + "type": "scatter", + "x": [ + "2026-03-09T00:00:00", + "2026-03-16T00:00:00", + "2026-03-31T00:00:00" + ], + "xaxis": "x3", + "y": [ + 65.4, + 65.8, + 66.3 + ], + "yaxis": "y3" + } + ], + "layout": { + "annotations": [ + { + "font": { + "size": 16 + }, + "showarrow": false, + "text": "Feb 02, 2026 WTI $62", + "x": 0.15333333333333335, + "xanchor": "center", + "xref": "paper", + "y": 1, + "yanchor": "bottom", + "yref": "paper" + }, + { + "font": { + "size": 16 + }, + "showarrow": false, + "text": "Feb 23, 2026 WTI $66", + "x": 0.5, + "xanchor": "center", + "xref": "paper", + "y": 1, + "yanchor": "bottom", + "yref": "paper" + }, + { + "font": { + "size": 16 + }, + "showarrow": false, + "text": "Mar 02, 2026 WTI $71", + "x": 0.8466666666666667, + "xanchor": "center", + "xref": "paper", + "y": 1, + "yanchor": "bottom", + "yref": "paper" + } + ], + "height": 420, + "legend": { + "font": { + "size": 12 + }, + "orientation": "h", + "x": 0, + "xanchor": "left", + "y": -0.12 + }, + "margin": { + "b": 50, + "l": 60, + "r": 20, + "t": 80 + }, + "shapes": [ + { + "line": { + "color": "#aaaaaa", + "dash": "dash", + "width": 1.2 + }, + "type": "line", + "x0": 1769990400000, + "x1": 1769990400000, + "xref": "x", + "y0": 0, + "y1": 1, + "yref": "y domain" + }, + { + "line": { + "color": "#aaaaaa", + "dash": "dash", + "width": 1.2 + }, + "type": "line", + "x0": 1771804800000, + "x1": 1771804800000, + "xref": "x2", + "y0": 0, + "y1": 1, + "yref": "y2 domain" + }, + { + "line": { + "color": "#aaaaaa", + "dash": "dash", + "width": 1.2 + }, + "type": "line", + "x0": 1772409600000, + "x1": 1772409600000, + "xref": "x3", + "y0": 0, + "y1": 1, + "yref": "y3 domain" + } + ], + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "#2a3f5f" + }, + "error_y": { + "color": "#2a3f5f" + }, + "marker": { + "line": { + "color": "white", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "white", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "#C8D4E3", + "linecolor": "#C8D4E3", + "minorgridcolor": "#C8D4E3", + "startlinecolor": "#2a3f5f" + }, + "baxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "#C8D4E3", + "linecolor": "#C8D4E3", + "minorgridcolor": "#C8D4E3", + "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": "white", + "showlakes": true, + "showland": true, + "subunitcolor": "#C8D4E3" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "white", + "polar": { + "angularaxis": { + "gridcolor": "#EBF0F8", + "linecolor": "#EBF0F8", + "ticks": "" + }, + "bgcolor": "white", + "radialaxis": { + "gridcolor": "#EBF0F8", + "linecolor": "#EBF0F8", + "ticks": "" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "white", + "gridcolor": "#DFE8F3", + "gridwidth": 2, + "linecolor": "#EBF0F8", + "showbackground": true, + "ticks": "", + "zerolinecolor": "#EBF0F8" + }, + "yaxis": { + "backgroundcolor": "white", + "gridcolor": "#DFE8F3", + "gridwidth": 2, + "linecolor": "#EBF0F8", + "showbackground": true, + "ticks": "", + "zerolinecolor": "#EBF0F8" + }, + "zaxis": { + "backgroundcolor": "white", + "gridcolor": "#DFE8F3", + "gridwidth": 2, + "linecolor": "#EBF0F8", + "showbackground": true, + "ticks": "", + "zerolinecolor": "#EBF0F8" + } + }, + "shapedefaults": { + "line": { + "color": "#2a3f5f" + } + }, + "ternary": { + "aaxis": { + "gridcolor": "#DFE8F3", + "linecolor": "#A2B1C6", + "ticks": "" + }, + "baxis": { + "gridcolor": "#DFE8F3", + "linecolor": "#A2B1C6", + "ticks": "" + }, + "bgcolor": "white", + "caxis": { + "gridcolor": "#DFE8F3", + "linecolor": "#A2B1C6", + "ticks": "" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "#EBF0F8", + "linecolor": "#EBF0F8", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "#EBF0F8", + "zerolinewidth": 2 + }, + "yaxis": { + "automargin": true, + "gridcolor": "#EBF0F8", + "linecolor": "#EBF0F8", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "#EBF0F8", + "zerolinewidth": 2 + } + } + }, + "title": { + "font": { + "size": 16 + }, + "text": "WTI Trajectory Forecast — Prophet Fan vs Agent Estimates", + "x": 0, + "xanchor": "left" + }, + "width": 1000, + "xaxis": { + "anchor": "y", + "domain": [ + 0, + 0.3066666666666667 + ], + "gridcolor": "#f0f0f0", + "showgrid": true, + "tickfont": { + "size": 11 + } + }, + "xaxis2": { + "anchor": "y2", + "domain": [ + 0.3466666666666667, + 0.6533333333333333 + ], + "gridcolor": "#f0f0f0", + "showgrid": true, + "tickfont": { + "size": 11 + } + }, + "xaxis3": { + "anchor": "y3", + "domain": [ + 0.6933333333333334, + 1 + ], + "gridcolor": "#f0f0f0", + "showgrid": true, + "tickfont": { + "size": 11 + } + }, + "yaxis": { + "anchor": "x", + "domain": [ + 0, + 1 + ], + "gridcolor": "#f0f0f0", + "showgrid": true, + "tickfont": { + "size": 11 + } + }, + "yaxis2": { + "anchor": "x2", + "domain": [ + 0, + 1 + ], + "gridcolor": "#f0f0f0", + "matches": "y", + "showgrid": true, + "showticklabels": false, + "tickfont": { + "size": 11 + } + }, + "yaxis3": { + "anchor": "x3", + "domain": [ + 0, + 1 + ], + "gridcolor": "#f0f0f0", + "matches": "y", + "showgrid": true, + "showticklabels": false, + "tickfont": { + "size": 11 + } + } + } + } + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Actual ($)Prophet ($)Agent ($)
OriginHorizon
2026-02-025 bdays64.462.665.2
10 bdays62.363.464.8
21 bdays74.665.264.0
2026-02-235 bdays71.264.666.8
10 bdays94.864.767.2
21 bdays92.364.467.5
2026-03-025 bdays94.864.665.4
10 bdays93.564.365.8
21 bdays101.464.566.3
\n", + "
" + ], + "text/plain": [ + " Actual ($) Prophet ($) Agent ($)\n", + "Origin Horizon \n", + "2026-02-02 5 bdays 64.4 62.6 65.2\n", + " 10 bdays 62.3 63.4 64.8\n", + " 21 bdays 74.6 65.2 64.0\n", + "2026-02-23 5 bdays 71.2 64.6 66.8\n", + " 10 bdays 94.8 64.7 67.2\n", + " 21 bdays 92.3 64.4 67.5\n", + "2026-03-02 5 bdays 94.8 64.6 65.4\n", + " 10 bdays 93.5 64.3 65.8\n", + " 21 bdays 101.4 64.5 66.3" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + "Mean MAE Prophet: $19.23 Agent: $18.09\n" + ] + } + ], "source": [ "# ── Trajectory fan chart: Prophet fan vs agent error bars at 3 origins ───────\n", "fig = make_trajectory_fan_chart(traj_agent_results, prophet_traj_df, price_df, TRAJECTORY_ORIGINS)\n", @@ -115,20 +2911,178 @@ }, { "cell_type": "markdown", - "id": "72eea5119410473aa328ad9291626812", + "id": "c3cb3841", "metadata": {}, "source": [ "---\n", - "## Stream 2 — Binary Shock Prediction" + "## Stream 2 — Binary Shock Prediction\n", + "\n", + "**Question:** What is P(WTI closes more than $5/bbl higher in 5 trading days)?\n", + "\n", + "Same identity. A different task spec. Edit the threshold or horizon wording below —\n", + "if you change the scored definition, also update `SHOCK_THRESHOLD` / `SHOCK_HORIZON`\n", + "so the scorer stays aligned.\n", + "\n", + "**Try this:** raise the bar to +$10 and compare probabilities.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "35162778", + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "### Task spec — Stream 2\n", + "\n", + "Estimate P(up) — the probability that WTI will close MORE THAN\n", + "$5/bbl HIGHER than today's price at the end of\n", + "5 trading days.\n", + "\n", + "This is a directional upside question only.\n", + "\n", + "Calibration guidance:\n", + " - No unusual upside catalyst -> base rate ~10-15%\n", + " - Escalating unconfirmed risk -> 20-40%\n", + " - Confirmed supply disruption -> 60-85%\n", + "\n", + "If a `set_model_response` tool is available, call it with your complete JSON as `json_response`. Otherwise return the JSON directly as plain text.\n", + "\n", + "**Required JSON format** (`DiscreteAgentForecastOutput`):\n", + "\n", + "```json\n", + "{\n", + " \"probability\": \"\",\n", + " \"direction_bias\": \"<'up' | 'down' | 'neutral'>\",\n", + " \"reasoning\": \"\",\n", + " \"key_signals\": [\n", + " \"\",\n", + " \"\"\n", + " ],\n", + " \"confidence\": \"<'high' | 'medium' | 'low'>\"\n", + "}\n", + "```" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# ── Stream 2 task spec (edit this) ────────────────────────────────────────────\n", + "# Scorer uses SHOCK_THRESHOLD / SHOCK_HORIZON from paths.py — keep them in sync.\n", + "_SHOCK_SCHEMA = DiscreteAgentForecastOutput.prompt_schema_json()\n", + "SHOCK_TASK_SPEC = f\"\"\"Estimate P(up) — the probability that WTI will close MORE THAN\n", + "${int(SHOCK_THRESHOLD)}/bbl HIGHER than today's price at the end of\n", + "{SHOCK_HORIZON} trading days.\n", + "\n", + "This is a directional upside question only.\n", + "\n", + "Calibration guidance:\n", + " - No unusual upside catalyst -> base rate ~10-15%\n", + " - Escalating unconfirmed risk -> 20-40%\n", + " - Confirmed supply disruption -> 60-85%\n", + "\n", + "If a `set_model_response` tool is available, call it with your complete JSON as `json_response`. Otherwise return the JSON directly as plain text.\n", + "\n", + "Required JSON format:\n", + "{_SHOCK_SCHEMA}\n", + "\"\"\"\n", + "\n", + "_prose, _, _schema = SHOCK_TASK_SPEC.partition(\"Required JSON format:\")\n", + "display(\n", + " Markdown(\n", + " \"### Task spec — Stream 2\\n\\n\"\n", + " + _prose.strip()\n", + " + \"\\n\\n**Required JSON format** (`DiscreteAgentForecastOutput`):\\n\\n```json\\n\"\n", + " + _schema.strip()\n", + " + \"\\n```\"\n", + " )\n", + ")" ] }, { "cell_type": "code", - "execution_count": null, - "id": "8edb47106e1a46a883d545849b8ab81b", + "execution_count": 9, + "id": "414a87bd", "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Predictor schema: DiscreteAgentForecastOutput\n" + ] + }, + { + "data": { + "text/markdown": [ + "### User payload preview *(as_of 2026-03-08, WTI $81.01/bbl)*\n", + "\n", + "This is how we assign the task: the ask rides in `task_spec`; horizons and quantiles come from the `ForecastingTask`.\n", + "\n", + "**Price history** — last 10 of 1256 rows:\n", + "\n", + "```\n", + "2026-02-20,66.39\n", + "2026-02-23,66.31\n", + "2026-02-24,65.63\n", + "2026-02-25,65.42\n", + "2026-02-26,65.21\n", + "2026-02-27,67.02\n", + "2026-03-02,71.23\n", + "2026-03-03,74.56\n", + "2026-03-04,74.66\n", + "2026-03-05,81.01\n", + "```\n", + "\n", + "**horizons:** `[5]` · **standard_quantiles:** 11 levels\n", + "\n", + "**task_spec** (742 chars) — prose:\n", + "\n", + "Estimate P(up) — the probability that WTI will close MORE THAN\n", + "$5/bbl HIGHER than today's price at the end of\n", + "5 trading days.\n", + "\n", + "This is a directional upside question only.\n", + "\n", + "Calibration guidance:\n", + " - No unusual upside catalyst -> base rate ~10-15%\n", + " - Escalating unconfirmed risk -> 20-40%\n", + " - Confirmed supply disruption -> 60-85%\n", + "\n", + "If a `set_model_response` tool is available, call it with your complete JSON as `json_response`. Otherwise return the JSON directly as plain text.\n", + "\n", + "**Required JSON format:**\n", + "\n", + "```json\n", + "{\n", + " \"probability\": \"\",\n", + " \"direction_bias\": \"<'up' | 'down' | 'neutral'>\",\n", + " \"reasoning\": \"\",\n", + " \"key_signals\": [\n", + " \"\",\n", + " \"\"\n", + " ],\n", + " \"confidence\": \"<'high' | 'medium' | 'low'>\"\n", + "}\n", + "```" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ + "# ── Assign role: wire identity + task spec (no model call) ────────────────────\n", "shock_task = ForecastingTask(\n", " task_id=\"wti_upshock_demo\",\n", " target_series_id=WTI_SERIES_ID,\n", @@ -136,9 +3090,35 @@ " frequency=\"B\",\n", " description=\"Binary upshock demo\",\n", ")\n", + "shock_prompt_builder = WtiMultitaskPromptBuilder(task_spec=SHOCK_TASK_SPEC)\n", + "shock_predictor = AgentPredictor(\n", + " agent_config=analyst_config,\n", + " prompt_builder=shock_prompt_builder,\n", + " output_schema=DiscreteAgentForecastOutput,\n", + ")\n", "\n", - "shock_predictor = build_wti_news_predictor(\"shock\", model=AGENT_MODEL)\n", - "\n", + "print(f\"Predictor schema: {shock_predictor.output_schema.__name__}\")\n", + "preview_user_payload(shock_prompt_builder, shock_task, SHOCK_ORIGINS[-1])" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "914c396f", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saved 6 shock forecasts.\n", + "Agent Brier score: 0.2425\n" + ] + } + ], + "source": [ + "# ── Run shock agent across origins ────────────────────────────────────────────\n", + "# Uses analyst_config + SHOCK_TASK_SPEC. Keep USE_CACHE = False after edits.\n", "if USE_CACHE and SHOCK_ANALYST_CACHE.exists():\n", " with open(SHOCK_ANALYST_CACHE) as f:\n", " shock_results = json.load(f)\n", @@ -161,27 +3141,1527 @@ " )\n", " with open(SHOCK_ANALYST_CACHE, \"w\") as f:\n", " json.dump(shock_results, f, indent=2)\n", + " print(f\"Saved {len(shock_results)} shock forecasts.\")\n", "\n", "agent_probs = [r[\"probability\"] for r in shock_results]\n", "outcomes = [r[\"outcome\"] for r in shock_results]\n", - "print(f\"Agent Brier score: {compute_brier_score(agent_probs, outcomes):.4f}\")\n", - "print(f\"Task spec preview:\\n{TASK_SPECS['shock'][:200]}...\")" + "print(f\"Agent Brier score: {compute_brier_score(agent_probs, outcomes):.4f}\")" ] }, { "cell_type": "code", - "execution_count": null, - "id": "2d478ced", + "execution_count": 11, + "id": "2df359f9", "metadata": {}, - "outputs": [], - "source": "# ── Per-origin forecast cards ─────────────────────────────────────────────────\nfor r in shock_results:\n origin = pd.Timestamp(r[\"origin\"])\n label = origin.strftime(\"%b %-d, %Y\")\n origin_price_row = price_df[price_df.index >= origin]\n origin_price = float(origin_price_row.iloc[0][\"price\"]) if not origin_price_row.empty else float(\"nan\")\n a_prob = float(r[\"probability\"])\n outcome = int(r[\"outcome\"])\n delta = float(r[\"delta\"])\n brier = (a_prob - outcome) ** 2\n meta = r.get(\"metadata\", {})\n reasoning = meta.get(\"rationale\", \"—\")\n key_signals = meta.get(\"key_signals\", [])\n confidence = meta.get(\"confidence\", \"?\")\n outcome_badge = \"**SHOCK**\" if outcome else \"No shock\"\n\n display(\n Markdown(\n f\"---\\n\"\n f\"### {label} — WTI ${origin_price:.2f}/bbl\\n\\n\"\n f\"| | |\\n|---|---|\\n\"\n f\"| **Prediction** | P(up > +${SHOCK_THRESHOLD:.0f}) = **{a_prob:.0%}** `{prob_bar(a_prob)}` |\\n\"\n f\"| **Confidence** | {confidence.title() if isinstance(confidence, str) else confidence} {conf_bar(str(confidence))} |\\n\"\n f\"| **Rationale** | {reasoning} |\\n\"\n f\"| **Key signals** | {' · '.join(key_signals) if key_signals else '—'} |\\n\"\n f\"| **Actual outcome** | {outcome_badge} — price moved **{delta:+.2f}/bbl** |\\n\"\n f\"| **Verdict** | {verdict_label(a_prob, outcome, delta, SHOCK_THRESHOLD)} |\\n\"\n f\"| **Brier score** | {brier:.3f} {'🟢' if brier < 0.10 else '🟡' if brier < 0.25 else '🔴'} |\\n\"\n )\n )" + "outputs": [ + { + "data": { + "text/markdown": [ + "---\n", + "### Feb 2, 2026 — WTI $62.14/bbl\n", + "\n", + "| | |\n", + "|---|---|\n", + "| **Prediction** | P(up > +$5) = **25%** `██░░░░░░░░ 25%` |\n", + "| **Confidence** | Medium 🟡 |\n", + "| **Rationale** | As of February 1, 2026, WTI is trading at ~$65.42/bbl. The market is currently characterized by a delicate balance between structural oversupply concerns and a significant, though unconfirmed, geopolitical risk premium. Specifically, tensions surrounding the Strait of Hormuz are contributing an estimated $4-$10/bbl to the current price. While a $5/bbl move upward (to >$70.42) in five trading days is substantial, it is well within the realm of volatility observed in this environment if a supply-related headline (e.g., an escalation in the Middle East or a physical disruption in the Strait) were to materialize. Given the calibration guidance, the presence of 'escalating unconfirmed risk' justifies a probability in the 20-40% range. |\n", + "| **Key signals** | Heightened geopolitical tension in the Middle East and the Strait of Hormuz · Estimated $4-$10/bbl risk premium currently embedded in WTI prices · OPEC+ production restraint maintaining a tight floor on prices · High market sensitivity to supply disruption headlines |\n", + "| **Actual outcome** | No shock — price moved **+2.22/bbl** |\n", + "| **Verdict** | Actual: +$2.22/bbl — no shock |\n", + "| **Brier score** | 0.062 🟢 |\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "---\n", + "### Feb 9, 2026 — WTI $64.36/bbl\n", + "\n", + "| | |\n", + "|---|---|\n", + "| **Prediction** | P(up > +$5) = **35%** `████░░░░░░ 35%` |\n", + "| **Confidence** | Medium 🟡 |\n", + "| **Rationale** | The market is currently driven by a significant geopolitical risk premium, with the Strait of Hormuz acting as a flashpoint between the US and Iran. While current prices have consolidated near $63/bbl after recent volatility, the risk of a sudden escalation that disrupts supply in the Strait is non-negligible. A $5/bbl move represents an approximate 8% increase, which is within the range of potential volatility jumps during periods of heightened geopolitical tension. Given the guidance for unconfirmed risk (20-40%), a 35% probability reflects that while an escalation is not certain, the market remains fragile to supply-side shocks. |\n", + "| **Key signals** | Heightened geopolitical risk premium related to US-Iran tensions near the Strait of Hormuz. · OPEC+ decision to pause production increments for March 2026, signaling a desire for price support. · Recent market volatility and sensitivity to news flows regarding maritime security in the Middle East. |\n", + "| **Actual outcome** | No shock — price moved **-2.03/bbl** |\n", + "| **Verdict** | Actual: +$-2.03/bbl — no shock |\n", + "| **Brier score** | 0.122 🟡 |\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "---\n", + "### Feb 16, 2026 — WTI $62.33/bbl\n", + "\n", + "| | |\n", + "|---|---|\n", + "| **Prediction** | P(up > +$5) = **35%** `████░░░░░░ 35%` |\n", + "| **Confidence** | Medium 🟡 |\n", + "| **Rationale** | The WTI market is currently heavily influenced by a 'geopolitical risk premium' related to tensions in the Middle East and the Strait of Hormuz. While there was a recent price decline in early February due to demand concerns and inventory data, the risk of a sudden, sharp supply disruption remains elevated. A $5/bbl move (roughly 8%) within 5 days is significant but plausible if there is a concrete escalation in the Strait of Hormuz or new, definitive news regarding Iranian supply. Given that the market is already sensitive to these risks, a probability of 35% sits between 'unconfirmed risk' and 'confirmed disruption', reflecting the potential for sudden volatility as tensions fluctuate. |\n", + "| **Key signals** | Heightened geopolitical tension in the Strait of Hormuz. · Existence of a $4-$10/bbl 'geopolitical risk premium' in current pricing. · OPEC+ policy to pause production increments in March 2026. · Recent reversal in sentiment following U.S. naval advisories in Iranian waters. |\n", + "| **Actual outcome** | No shock — price moved **+3.98/bbl** |\n", + "| **Verdict** | Actual: +$3.98/bbl — no shock |\n", + "| **Brier score** | 0.122 🟡 |\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "---\n", + "### Feb 23, 2026 — WTI $66.31/bbl\n", + "\n", + "| | |\n", + "|---|---|\n", + "| **Prediction** | P(up > +$5) = **15%** `██░░░░░░░░ 15%` |\n", + "| **Confidence** | Medium 🟡 |\n", + "| **Rationale** | While there is a geopolitical risk premium associated with tensions in the Middle East, specifically around the Strait of Hormuz, the market has recently been tempered by IEA forecasts of lower demand and higher U.S. inventories. Prices have remained in a range, and while volatility is high, there is no immediate 'confirmed' supply disruption that would justify the high threshold of a $5/bbl increase in just 5 trading days. The current price of $66.43 represents the upper end of the recent consolidation; without a specific catalyst to trigger an breakout, a move to $71.43+ within a week is unlikely to be the base-case outcome. |\n", + "| **Key signals** | Heightened geopolitical risk premium (Strait of Hormuz tensions) provides a structural floor but is currently priced in. · IEA demand concerns and rising U.S. inventory levels act as a dampener on aggressive upside moves. · OPEC+ production policy remains cautious and flexible, preventing panic buying. |\n", + "| **Actual outcome** | No shock — price moved **+4.92/bbl** |\n", + "| **Verdict** | Actual: +$4.92/bbl — no shock |\n", + "| **Brier score** | 0.022 🟢 |\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "---\n", + "### Mar 2, 2026 — WTI $71.23/bbl\n", + "\n", + "| | |\n", + "|---|---|\n", + "| **Prediction** | P(up > +$5) = **25%** `██░░░░░░░░ 25%` |\n", + "| **Confidence** | Medium 🟡 |\n", + "| **Rationale** | As of March 1, 2026, the WTI price of ~$65.21 is trading within a context of heightened geopolitical anxiety, specifically regarding U.S.-Iran tensions and potential disruptions at the Strait of Hormuz. While the underlying physical market is adequately supplied, the 'geopolitical risk premium' is significant. A move to >$70.21 (a $5/bbl increase) within 5 trading days would require either a concrete escalation in physical disruption or a sharp tightening of market sentiment. Given the volatility, a 25% probability accounts for the high sensitivity to news, placing it between 'no unusual catalyst' and 'confirmed supply disruption'. |\n", + "| **Key signals** | Escalating tensions between the U.S. and Iran · Market sensitivity regarding the Strait of Hormuz · OPEC+ commitment to production restraint, limiting downside inventory pressure |\n", + "| **Actual outcome** | **SHOCK** — price moved **+23.54/bbl** |\n", + "| **Verdict** | Actual: +$23.54/bbl (>5) — shock materialised |\n", + "| **Brier score** | 0.562 🔴 |\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "---\n", + "### Mar 9, 2026 — WTI $94.77/bbl\n", + "\n", + "| | |\n", + "|---|---|\n", + "| **Prediction** | P(up > +$5) = **75%** `████████░░ 75%` |\n", + "| **Confidence** | High 🟢 |\n", + "| **Rationale** | The WTI market is currently facing a confirmed, high-impact supply disruption following the closure of the Strait of Hormuz in late February 2026. This chokepoint handles ~20% of global seaborne oil trade. With air strikes continuing and tankers avoiding the region due to war-risk, market participants are pricing in an extreme geopolitical risk premium. A move of >$5/bbl (approx. 6%) over a 5-day horizon is highly plausible given the ongoing instability, the lack of a clear timeline for the reopening of the strait, and the psychological impact of the IRGC's activities. This aligns with the 'confirmed supply disruption' category for high-probability outcomes. |\n", + "| **Key signals** | Continued closure of the Strait of Hormuz · Extreme volatility due to the US-Iran military conflict · Global shipping rerouting and imposition of war-risk surcharges |\n", + "| **Actual outcome** | No shock — price moved **-1.27/bbl** |\n", + "| **Verdict** | Actual: +$-1.27/bbl — no shock |\n", + "| **Brier score** | 0.562 🔴 |\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# ── Per-origin forecast cards ─────────────────────────────────────────────────\n", + "for r in shock_results:\n", + " origin = pd.Timestamp(r[\"origin\"])\n", + " label = origin.strftime(\"%b %-d, %Y\")\n", + " origin_price_row = price_df[price_df.index >= origin]\n", + " origin_price = float(origin_price_row.iloc[0][\"price\"]) if not origin_price_row.empty else float(\"nan\")\n", + " a_prob = float(r[\"probability\"])\n", + " outcome = int(r[\"outcome\"])\n", + " delta = float(r[\"delta\"])\n", + " brier = (a_prob - outcome) ** 2\n", + " meta = r.get(\"metadata\", {})\n", + " reasoning = meta.get(\"rationale\", \"—\")\n", + " key_signals = meta.get(\"key_signals\", [])\n", + " confidence = meta.get(\"confidence\", \"?\")\n", + " outcome_badge = \"**SHOCK**\" if outcome else \"No shock\"\n", + "\n", + " display(\n", + " Markdown(\n", + " f\"---\\n\"\n", + " f\"### {label} — WTI ${origin_price:.2f}/bbl\\n\\n\"\n", + " f\"| | |\\n|---|---|\\n\"\n", + " f\"| **Prediction** | P(up > +${SHOCK_THRESHOLD:.0f}) = **{a_prob:.0%}** `{prob_bar(a_prob)}` |\\n\"\n", + " f\"| **Confidence** | {confidence.title() if isinstance(confidence, str) else confidence} {conf_bar(str(confidence))} |\\n\"\n", + " f\"| **Rationale** | {reasoning} |\\n\"\n", + " f\"| **Key signals** | {' · '.join(key_signals) if key_signals else '—'} |\\n\"\n", + " f\"| **Actual outcome** | {outcome_badge} — price moved **{delta:+.2f}/bbl** |\\n\"\n", + " f\"| **Verdict** | {verdict_label(a_prob, outcome, delta, SHOCK_THRESHOLD)} |\\n\"\n", + " f\"| **Brier score** | {brier:.3f} {'🟢' if brier < 0.10 else '🟡' if brier < 0.25 else '🔴'} |\\n\"\n", + " )\n", + " )" + ] }, { "cell_type": "code", - "execution_count": null, - "id": "fb5a842f", + "execution_count": 12, + "id": "1a5c19f8", "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.plotly.v1+json": { + "config": { + "plotlyServerURL": "https://plot.ly" + }, + "data": [ + { + "hovertemplate": "%{x}
P(shock)=%{y:.0%}Analyst Agent", + "legendgroup": "Analyst Agent", + "line": { + "color": "#2ca02c", + "dash": "solid", + "width": 2.5 + }, + "marker": { + "size": 10, + "symbol": "circle" + }, + "mode": "lines+markers", + "name": "Analyst Agent", + "showlegend": true, + "type": "scatter", + "x": [ + "2026-02-02", + "2026-02-09", + "2026-02-16", + "2026-02-23", + "2026-03-02", + "2026-03-09" + ], + "xaxis": "x", + "y": [ + 0.25, + 0.35, + 0.35, + 0.15, + 0.25, + 0.75 + ], + "yaxis": "y" + }, + { + "hovertemplate": "%{x}
P(shock)=%{y:.0%}Prophet", + "legendgroup": "Prophet", + "line": { + "color": "#636363", + "dash": "dot", + "width": 2.5 + }, + "marker": { + "size": 10, + "symbol": "square" + }, + "mode": "lines+markers", + "name": "Prophet", + "showlegend": true, + "type": "scatter", + "x": [ + "2026-02-02", + "2026-02-09", + "2026-02-16", + "2026-02-23", + "2026-03-02", + "2026-03-09" + ], + "xaxis": "x", + "y": [ + 0.3109203743332908, + 0.25557354149880673, + 0.35712312662621903, + 0.23140737717243345, + 0.09828085161892997, + 0.000021399895970941607 + ], + "yaxis": "y" + }, + { + "hovertemplate": "%{x}
Cumul. Brier: %{y:.3f}Analyst Agent", + "legendgroup": "Analyst Agent", + "line": { + "color": "#2ca02c", + "dash": "solid", + "width": 2.5 + }, + "marker": { + "size": 8, + "symbol": "circle" + }, + "mode": "lines+markers", + "name": "Analyst Agent", + "showlegend": false, + "type": "scatter", + "x": [ + "2026-02-02", + "2026-02-09", + "2026-02-16", + "2026-02-23", + "2026-03-02", + "2026-03-09" + ], + "xaxis": "x2", + "y": [ + 0.0625, + 0.0925, + 0.1025, + 0.0825, + 0.17850000000000002, + 0.2425 + ], + "yaxis": "y2" + }, + { + "hovertemplate": "%{x}
Cumul. Brier: %{y:.3f}Prophet", + "legendgroup": "Prophet", + "line": { + "color": "#636363", + "dash": "dot", + "width": 2.5 + }, + "marker": { + "size": 8, + "symbol": "square" + }, + "mode": "lines+markers", + "name": "Prophet", + "showlegend": false, + "type": "scatter", + "x": [ + "2026-02-02", + "2026-02-09", + "2026-02-16", + "2026-02-23", + "2026-03-02", + "2026-03-09" + ], + "xaxis": "x2", + "y": [ + 0.09667147917555366, + 0.08099465714489798, + 0.09650874728702748, + 0.08576890401772683, + 0.23123460772559792, + 0.19269550651432416 + ], + "yaxis": "y2" + } + ], + "layout": { + "annotations": [ + { + "font": { + "size": 16 + }, + "showarrow": false, + "text": "P(WTI up > +$5/bbl in 5 trading days)", + "x": 0.5, + "xanchor": "center", + "xref": "paper", + "y": 1, + "yanchor": "bottom", + "yref": "paper" + }, + { + "font": { + "size": 16 + }, + "showarrow": false, + "text": "Cumulative mean Brier score (lower = better)", + "x": 0.5, + "xanchor": "center", + "xref": "paper", + "y": 0.3276, + "yanchor": "bottom", + "yref": "paper" + }, + { + "font": { + "color": "#d62728", + "size": 9 + }, + "showarrow": false, + "text": "SHOCK", + "x": "2026-03-02", + "xref": "x", + "y": 1.04, + "yref": "y" + }, + { + "font": { + "color": "#2ca02c", + "size": 8 + }, + "showarrow": false, + "text": "25%", + "x": "2026-02-02", + "xref": "x", + "y": 0.25, + "yref": "y", + "yshift": 12 + }, + { + "font": { + "color": "#2ca02c", + "size": 8 + }, + "showarrow": false, + "text": "35%", + "x": "2026-02-09", + "xref": "x", + "y": 0.35, + "yref": "y", + "yshift": 12 + }, + { + "font": { + "color": "#2ca02c", + "size": 8 + }, + "showarrow": false, + "text": "35%", + "x": "2026-02-16", + "xref": "x", + "y": 0.35, + "yref": "y", + "yshift": 12 + }, + { + "font": { + "color": "#2ca02c", + "size": 8 + }, + "showarrow": false, + "text": "15%", + "x": "2026-02-23", + "xref": "x", + "y": 0.15, + "yref": "y", + "yshift": 12 + }, + { + "font": { + "color": "#2ca02c", + "size": 8 + }, + "showarrow": false, + "text": "25%", + "x": "2026-03-02", + "xref": "x", + "y": 0.25, + "yref": "y", + "yshift": 12 + }, + { + "font": { + "color": "#2ca02c", + "size": 8 + }, + "showarrow": false, + "text": "75%", + "x": "2026-03-09", + "xref": "x", + "y": 0.75, + "yref": "y", + "yshift": 12 + }, + { + "font": { + "color": "#636363", + "size": 8 + }, + "showarrow": false, + "text": "31%", + "x": "2026-02-02", + "xref": "x", + "y": 0.3109203743332908, + "yref": "y", + "yshift": -14 + }, + { + "font": { + "color": "#636363", + "size": 8 + }, + "showarrow": false, + "text": "26%", + "x": "2026-02-09", + "xref": "x", + "y": 0.25557354149880673, + "yref": "y", + "yshift": -14 + }, + { + "font": { + "color": "#636363", + "size": 8 + }, + "showarrow": false, + "text": "36%", + "x": "2026-02-16", + "xref": "x", + "y": 0.35712312662621903, + "yref": "y", + "yshift": -14 + }, + { + "font": { + "color": "#636363", + "size": 8 + }, + "showarrow": false, + "text": "23%", + "x": "2026-02-23", + "xref": "x", + "y": 0.23140737717243345, + "yref": "y", + "yshift": -14 + }, + { + "font": { + "color": "#636363", + "size": 8 + }, + "showarrow": false, + "text": "10%", + "x": "2026-03-02", + "xref": "x", + "y": 0.09828085161892997, + "yref": "y", + "yshift": -14 + }, + { + "font": { + "color": "#636363", + "size": 8 + }, + "showarrow": false, + "text": "0%", + "x": "2026-03-09", + "xref": "x", + "y": 0.000021399895970941607, + "yref": "y", + "yshift": -14 + }, + { + "font": { + "color": "#888888", + "size": 9 + }, + "showarrow": false, + "text": "0.25 random ceiling", + "x": 1, + "xanchor": "right", + "xref": "x2 domain", + "y": 0.25, + "yanchor": "bottom", + "yref": "y2" + } + ], + "height": 520, + "legend": { + "font": { + "size": 11 + }, + "orientation": "h", + "x": 1, + "xanchor": "right", + "y": 1.04, + "yanchor": "bottom" + }, + "margin": { + "b": 70, + "l": 60, + "r": 35, + "t": 80 + }, + "shapes": [ + { + "fillcolor": "rgba(214,39,40,0.12)", + "layer": "below", + "line": { + "width": 0 + }, + "type": "rect", + "x0": 3.52, + "x1": 4.48, + "xref": "x", + "y0": -0.06, + "y1": 1.06, + "yref": "y" + }, + { + "fillcolor": "rgba(214,39,40,0.12)", + "layer": "below", + "line": { + "width": 0 + }, + "type": "rect", + "x0": 3.52, + "x1": 4.48, + "xref": "x2", + "y0": 0, + "y1": 0.3, + "yref": "y2" + }, + { + "line": { + "color": "#d0d0d0", + "dash": "dot", + "width": 1.2 + }, + "type": "line", + "x0": 0, + "x1": 1, + "xref": "x domain", + "y0": 0.5, + "y1": 0.5, + "yref": "y" + }, + { + "line": { + "color": "#aaaaaa", + "dash": "dot", + "width": 1.5 + }, + "type": "line", + "x0": 0, + "x1": 1, + "xref": "x2 domain", + "y0": 0.25, + "y1": 0.25, + "yref": "y2" + } + ], + "template": { + "data": { + "bar": [ + { + "error_x": { + "color": "#2a3f5f" + }, + "error_y": { + "color": "#2a3f5f" + }, + "marker": { + "line": { + "color": "white", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "bar" + } + ], + "barpolar": [ + { + "marker": { + "line": { + "color": "white", + "width": 0.5 + }, + "pattern": { + "fillmode": "overlay", + "size": 10, + "solidity": 0.2 + } + }, + "type": "barpolar" + } + ], + "carpet": [ + { + "aaxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "#C8D4E3", + "linecolor": "#C8D4E3", + "minorgridcolor": "#C8D4E3", + "startlinecolor": "#2a3f5f" + }, + "baxis": { + "endlinecolor": "#2a3f5f", + "gridcolor": "#C8D4E3", + "linecolor": "#C8D4E3", + "minorgridcolor": "#C8D4E3", + "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": "white", + "showlakes": true, + "showland": true, + "subunitcolor": "#C8D4E3" + }, + "hoverlabel": { + "align": "left" + }, + "hovermode": "closest", + "mapbox": { + "style": "light" + }, + "paper_bgcolor": "white", + "plot_bgcolor": "white", + "polar": { + "angularaxis": { + "gridcolor": "#EBF0F8", + "linecolor": "#EBF0F8", + "ticks": "" + }, + "bgcolor": "white", + "radialaxis": { + "gridcolor": "#EBF0F8", + "linecolor": "#EBF0F8", + "ticks": "" + } + }, + "scene": { + "xaxis": { + "backgroundcolor": "white", + "gridcolor": "#DFE8F3", + "gridwidth": 2, + "linecolor": "#EBF0F8", + "showbackground": true, + "ticks": "", + "zerolinecolor": "#EBF0F8" + }, + "yaxis": { + "backgroundcolor": "white", + "gridcolor": "#DFE8F3", + "gridwidth": 2, + "linecolor": "#EBF0F8", + "showbackground": true, + "ticks": "", + "zerolinecolor": "#EBF0F8" + }, + "zaxis": { + "backgroundcolor": "white", + "gridcolor": "#DFE8F3", + "gridwidth": 2, + "linecolor": "#EBF0F8", + "showbackground": true, + "ticks": "", + "zerolinecolor": "#EBF0F8" + } + }, + "shapedefaults": { + "line": { + "color": "#2a3f5f" + } + }, + "ternary": { + "aaxis": { + "gridcolor": "#DFE8F3", + "linecolor": "#A2B1C6", + "ticks": "" + }, + "baxis": { + "gridcolor": "#DFE8F3", + "linecolor": "#A2B1C6", + "ticks": "" + }, + "bgcolor": "white", + "caxis": { + "gridcolor": "#DFE8F3", + "linecolor": "#A2B1C6", + "ticks": "" + } + }, + "title": { + "x": 0.05 + }, + "xaxis": { + "automargin": true, + "gridcolor": "#EBF0F8", + "linecolor": "#EBF0F8", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "#EBF0F8", + "zerolinewidth": 2 + }, + "yaxis": { + "automargin": true, + "gridcolor": "#EBF0F8", + "linecolor": "#EBF0F8", + "ticks": "", + "title": { + "standoff": 15 + }, + "zerolinecolor": "#EBF0F8", + "zerolinewidth": 2 + } + } + }, + "title": { + "font": { + "size": 13 + }, + "text": "Analyst Agent vs Prophet — WTI Upward Shock (>$5/bbl in 5 days)", + "x": 0.5 + }, + "width": 700, + "xaxis": { + "anchor": "y", + "domain": [ + 0, + 1 + ], + "showgrid": false, + "tickangle": -35, + "type": "category" + }, + "xaxis2": { + "anchor": "y2", + "domain": [ + 0, + 1 + ], + "showgrid": false, + "tickangle": -35, + "type": "category" + }, + "yaxis": { + "anchor": "x", + "domain": [ + 0.5476, + 1 + ], + "gridcolor": "#f0f0f0", + "range": [ + -0.06, + 1.12 + ], + "showgrid": true, + "tickformat": ".0%" + }, + "yaxis2": { + "anchor": "x2", + "domain": [ + 0, + 0.3276 + ], + "gridcolor": "#f0f0f0", + "range": [ + 0, + 0.32 + ], + "showgrid": true + } + } + } + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Mean Brier score (lower = better, 0.25 = random ceiling):\n" + ] + }, + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
Mean Brier score
Method
Analyst Agent0.2425
Prophet0.1927
\n", + "
" + ], + "text/plain": [ + " Mean Brier score\n", + "Method \n", + "Analyst Agent 0.2425\n", + "Prophet 0.1927" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "# ── Prophet probabilities for the shock origins ───────────────────────────────\n", "prophet_shock_probs = []\n", @@ -212,36 +4692,425 @@ }, { "cell_type": "markdown", - "id": "10185d26023b46108eb7d9f57d49d2b3", + "id": "fbd0dd1b", "metadata": {}, "source": [ "---\n", - "## Stream 3 — Scenario Analysis\n" + "## Stream 3 — Scenario Analysis\n", + "\n", + "**Question:** What three scenarios are oil-market analysts debating for WTI over the next 60 days?\n", + "\n", + "Same identity. Track 2 structured qualitative analysis — no ground truth to score.\n", + "Edit the task spec (number of scenarios, framing) or the origin, then re-run.\n", + "\n", + "**Try this:** change \"three scenarios\" to \"two bullish and one bearish\", or set\n", + "`SCENARIO_AS_OF = pd.Timestamp(\"2026-02-02\")` (pre-shock) and compare.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "id": "bb5d02a8", + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "### Task spec — Stream 3 *(origin 2026-02-02, WTI $62.14/bbl)*\n", + "\n", + "Identify the three scenarios that oil market analysts and experts are most\n", + "actively debating for WTI crude over the next 60 days, given the current\n", + "market context and price history.\n", + "\n", + "For each scenario:\n", + " - Give it a concise name (3-6 words)\n", + " - Describe it in 1-2 sentences\n", + " - Assign a probability (all three must sum to <= 1.0)\n", + " - Provide an expected WTI price range at the 60-day horizon as [low, high]\n", + " - Give your point estimate for WTI at 60 days under this scenario\n", + " - List 1-2 key drivers that would cause this scenario to materialise\n", + "\n", + "Also identify which scenario is the base case and provide an overall\n", + "one-paragraph reasoning summary.\n", + "\n", + "If a `set_model_response` tool is available, call it with your complete JSON as `json_response`. Otherwise return the JSON directly as plain text.\n", + "\n", + "**Required JSON format** (`ScenarioAgentForecastOutput`):\n", + "\n", + "```json\n", + "{\n", + " \"scenarios\": [\n", + " {\n", + " \"name\": \"\",\n", + " \"description\": \"\",\n", + " \"probability\": \"\",\n", + " \"wti_range_60d\": [\n", + " \"\",\n", + " \"\"\n", + " ],\n", + " \"point_estimate_60d\": \"\",\n", + " \"key_drivers\": [\n", + " \"\",\n", + " \"\"\n", + " ]\n", + " }\n", + " ],\n", + " \"base_case\": \"\",\n", + " \"reasoning\": \"\"\n", + "}\n", + "```" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# ── Stream 3 task spec (edit this) ────────────────────────────────────────────\n", + "SCENARIO_AS_OF = SCENARIO_ORIGIN # 2026-03-02 — conflict onset\n", + "# SCENARIO_AS_OF = pd.Timestamp(\"2026-02-02\") # pre-shock, quieter market\n", + "# SCENARIO_AS_OF = pd.Timestamp.today() # live — no deep historical fence\n", + "\n", + "_SCENARIO_SCHEMA = ScenarioAgentForecastOutput.prompt_schema_json()\n", + "SCENARIO_TASK_SPEC = f\"\"\"Identify the three scenarios that oil market analysts and experts are most\n", + "actively debating for WTI crude over the next 60 days, given the current\n", + "market context and price history.\n", + "\n", + "For each scenario:\n", + " - Give it a concise name (3-6 words)\n", + " - Describe it in 1-2 sentences\n", + " - Assign a probability (all three must sum to <= 1.0)\n", + " - Provide an expected WTI price range at the 60-day horizon as [low, high]\n", + " - Give your point estimate for WTI at 60 days under this scenario\n", + " - List 1-2 key drivers that would cause this scenario to materialise\n", + "\n", + "Also identify which scenario is the base case and provide an overall\n", + "one-paragraph reasoning summary.\n", + "\n", + "If a `set_model_response` tool is available, call it with your complete JSON as `json_response`. Otherwise return the JSON directly as plain text.\n", + "\n", + "Required JSON format:\n", + "{_SCENARIO_SCHEMA}\n", + "\"\"\"\n", + "\n", + "_origin_price_row = price_df[price_df.index >= SCENARIO_AS_OF]\n", + "_origin_price = float(_origin_price_row.iloc[0][\"price\"]) if not _origin_price_row.empty else float(\"nan\")\n", + "_prose, _, _schema = SCENARIO_TASK_SPEC.partition(\"Required JSON format:\")\n", + "display(\n", + " Markdown(\n", + " f\"### Task spec — Stream 3 *(origin {SCENARIO_AS_OF.date()}, WTI ${_origin_price:.2f}/bbl)*\\n\\n\"\n", + " + _prose.strip()\n", + " + \"\\n\\n**Required JSON format** (`ScenarioAgentForecastOutput`):\\n\\n```json\\n\"\n", + " + _schema.strip()\n", + " + \"\\n```\"\n", + " )\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "id": "2bb25d8b", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Predictor schema: ScenarioAgentForecastOutput\n" + ] + }, + { + "data": { + "text/markdown": [ + "### User payload preview *(as_of 2026-02-01, WTI $65.42/bbl)*\n", + "\n", + "This is how we assign the task: the ask rides in `task_spec`; horizons and quantiles come from the `ForecastingTask`.\n", + "\n", + "**Price history** — last 10 of 1254 rows:\n", + "\n", + "```\n", + "2026-01-15,59.19\n", + "2026-01-16,59.44\n", + "2026-01-20,60.34\n", + "2026-01-21,60.62\n", + "2026-01-22,59.36\n", + "2026-01-23,61.07\n", + "2026-01-26,60.63\n", + "2026-01-27,62.39\n", + "2026-01-28,63.21\n", + "2026-01-29,65.42\n", + "```\n", + "\n", + "**horizons:** `[21]` · **standard_quantiles:** 11 levels\n", + "\n", + "**task_spec** (1215 chars) — prose:\n", + "\n", + "Identify the three scenarios that oil market analysts and experts are most\n", + "actively debating for WTI crude over the next 60 days, given the current\n", + "market context and price history.\n", + "\n", + "For each scenario:\n", + " - Give it a concise name (3-6 words)\n", + " - Describe it in 1-2 sentences\n", + " - Assign a probability (all three must sum to <= 1.0)\n", + " - Provide an expected WTI price range at the 60-day horizon as [low, high]\n", + " - Give your point estimate for WTI at 60 days under this scenario\n", + " - List 1-2 key drivers that would cause this scenario to materialise\n", + "\n", + "Also identify which scenario is the base case and provide an overall\n", + "one-paragraph reasoning summary.\n", + "\n", + "If a `set_model_response` tool is available, call it with your complete JSON as `json_response`. Otherwise return the JSON directly as plain text.\n", + "\n", + "**Required JSON format:**\n", + "\n", + "```json\n", + "{\n", + " \"scenarios\": [\n", + " {\n", + " \"name\": \"\",\n", + " \"description\": \"\",\n", + " \"probability\": \"\",\n", + " \"wti_range_60d\": [\n", + " \"\",\n", + " \"\"\n", + " ],\n", + " \"point_estimate_60d\": \"\",\n", + " \"key_drivers\": [\n", + " \"\",\n", + " \"\"\n", + " ]\n", + " }\n", + " ],\n", + " \"base_case\": \"\",\n", + " \"reasoning\": \"\"\n", + "}\n", + "```" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# ── Assign role: wire identity + task spec (no model call) ────────────────────\n", + "scenario_task = ForecastingTask(\n", + " task_id=\"wti_scenario_demo\",\n", + " target_series_id=WTI_SERIES_ID,\n", + " horizons=[21], # ForecastingTask requires a horizon; the 60-day ask lives in the spec\n", + " frequency=\"B\",\n", + " description=\"Scenario analysis demo\",\n", + ")\n", + "scenario_prompt_builder = WtiMultitaskPromptBuilder(task_spec=SCENARIO_TASK_SPEC)\n", + "scenario_predictor = AgentPredictor(\n", + " agent_config=analyst_config,\n", + " prompt_builder=scenario_prompt_builder,\n", + " output_schema=ScenarioAgentForecastOutput,\n", + ")\n", + "\n", + "print(f\"Predictor schema: {scenario_predictor.output_schema.__name__}\")\n", + "preview_user_payload(scenario_prompt_builder, scenario_task, SCENARIO_AS_OF)" ] }, { "cell_type": "code", - "execution_count": null, - "id": "8763a12b2bbd4a93a75aff182afb95dc", + "execution_count": 21, + "id": "6e0e0d7f", "metadata": {}, - "outputs": [], - "source": "scenario_task = ForecastingTask(\n task_id=\"wti_scenario_demo\",\n target_series_id=WTI_SERIES_ID,\n horizons=[21],\n frequency=\"B\",\n description=\"Scenario analysis demo\",\n)\n\nscenario_predictor = build_wti_news_predictor(\"scenario\", model=AGENT_MODEL)\n\nif USE_CACHE and SCENARIO_CACHE.exists():\n with open(SCENARIO_CACHE) as f:\n scenario_payload = json.load(f)\n print(\"Loaded cached scenario analysis.\")\nelse:\n as_of = SCENARIO_ORIGIN - pd.Timedelta(days=1)\n origin_ctx = data_service.context(as_of=as_of)\n preds = scenario_predictor.predict(scenario_task, origin_ctx)\n scenario_payload = preds[0].metadata\n with open(SCENARIO_CACHE, \"w\") as f:\n json.dump(scenario_payload, f, indent=2)\n\n# ── Rich scenario cards ───────────────────────────────────────────────────────\nscenario_origin_price_row = price_df[price_df.index >= SCENARIO_ORIGIN]\nscenario_origin_price = (\n float(scenario_origin_price_row.iloc[0][\"price\"]) if not scenario_origin_price_row.empty else float(\"nan\")\n)\n\ndisplay(\n Markdown(\n f\"#### Stream 3 — Scenario Analysis \"\n f\"*(origin: {SCENARIO_ORIGIN.date()}, WTI ${scenario_origin_price:.2f}/bbl)*\\n\\n\"\n f\"Base case: **{scenario_payload.get('base_case', '?')}**\"\n )\n)\n\nbase_case = scenario_payload.get(\"base_case\", \"\")\nfor s in scenario_payload.get(\"scenarios\", []):\n name = s.get(\"name\", \"?\")\n desc = s.get(\"description\", \"\")\n prob = float(s.get(\"probability\", 0))\n rng = s.get(\"wti_range_60d\", [float(\"nan\"), float(\"nan\")])\n lo_r, hi_r = float(rng[0]), float(rng[1])\n pe = float(s.get(\"point_estimate_60d\", float(\"nan\")))\n drivers = s.get(\"key_drivers\", [])\n base_marker = \" ★ **base case**\" if name == base_case else \"\"\n\n display(\n Markdown(\n f\"---\\n\"\n f\"**{name}**{base_marker}\\n\\n\"\n f\"{desc}\\n\\n\"\n f\"| | |\\n|---|---|\\n\"\n f\"| Probability | **{prob:.0%}** `{prob_bar(prob)}` |\\n\"\n f\"| WTI range (60 days) | ${lo_r:.0f} – ${hi_r:.0f} /bbl |\\n\"\n f\"| Point estimate | **${pe:.0f} /bbl** |\\n\"\n f\"| Key drivers | {' · '.join(drivers) if drivers else '—'} |\\n\"\n )\n )\n\noverall = scenario_payload.get(\"rationale\", \"\")\nif overall:\n display(Markdown(f\"---\\n\\n> **Overall reasoning:** {overall}\"))" + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Saved scenario analysis.\n" + ] + }, + { + "data": { + "text/markdown": [ + "#### Agent response — Stream 3 *(origin: 2026-02-02, WTI $62.14/bbl)*\n", + "\n", + "Base case: **Market Fundamentals Balance Prevails**" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "---\n", + "**Market Fundamentals Balance Prevails** ★ **base case**\n", + "\n", + "The market remains range-bound as strong non-OPEC production offsets concerns over OPEC+ supply discipline and tepid global demand growth.\n", + "\n", + "| | |\n", + "|---|---|\n", + "| Probability | **50%** `█████░░░░░ 50%` |\n", + "| WTI range (60 days) | $60 – $68 /bbl |\n", + "| Point estimate | **$64 /bbl** |\n", + "| Key drivers | Consistent growth in non-OPEC crude output · OPEC+ successfully maintains current production caps |\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "---\n", + "**Geopolitical Escalation Risk Premium**\n", + "\n", + "Heightened tensions in the Middle East or energy-producing regions trigger a rally as market participants price in a sustained supply disruption premium.\n", + "\n", + "| | |\n", + "|---|---|\n", + "| Probability | **30%** `███░░░░░░░ 30%` |\n", + "| WTI range (60 days) | $67 – $75 /bbl |\n", + "| Point estimate | **$71 /bbl** |\n", + "| Key drivers | Sudden escalation in Middle East hostilities · Significant disruption to Iranian or Venezuelan export logistics |\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "---\n", + "**Demand-Led Cyclical Downturn**\n", + "\n", + "Signs of slowing global economic growth and high inventories push prices downward as the market loses confidence in OPEC+ intervention efficacy.\n", + "\n", + "| | |\n", + "|---|---|\n", + "| Probability | **20%** `██░░░░░░░░ 20%` |\n", + "| WTI range (60 days) | $55 – $62 /bbl |\n", + "| Point estimate | **$58 /bbl** |\n", + "| Key drivers | Weakening manufacturing and consumer demand indicators · Higher-than-expected rise in global oil storage levels |\n" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "---\n", + "\n", + "> **Overall reasoning:** As of February 2026, WTI sits at approximately $65/bbl, balancing between ongoing structural supply abundance from non-OPEC sources and significant, yet intermittent, geopolitical risk premiums. The base case reflects an environment where market fundamentals—dominated by sufficient global supply and OPEC+’s cautious, disciplined output stance—largely contain volatility. While geopolitical shocks create occasional price spikes, the lack of actual, sustained physical supply loss means the market remains anchored near its recent range. The other scenarios represent the tails of this distribution: either a materialization of supply disruptions or an accumulation of bearish macro data that forces a re-evaluation of demand." + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# ── Run the scenario agent ────────────────────────────────────────────────────\n", + "# Uses analyst_config + SCENARIO_TASK_SPEC. Keep USE_CACHE = False after edits.\n", + "if USE_CACHE:\n", + " print(\"USE_CACHE is True — cached cards ignore edits to the task spec.\")\n", + "\n", + "if USE_CACHE and SCENARIO_CACHE.exists():\n", + " with open(SCENARIO_CACHE) as f:\n", + " scenario_payload = json.load(f)\n", + " print(\"Loaded cached scenario analysis.\")\n", + "else:\n", + " as_of = SCENARIO_AS_OF - pd.Timedelta(days=1)\n", + " origin_ctx = data_service.context(as_of=as_of)\n", + " preds = scenario_predictor.predict(scenario_task, origin_ctx)\n", + " scenario_payload = preds[0].metadata\n", + " with open(SCENARIO_CACHE, \"w\") as f:\n", + " json.dump(scenario_payload, f, indent=2)\n", + " print(\"Saved scenario analysis.\")\n", + "\n", + "# ── Scenario cards ────────────────────────────────────────────────────────────\n", + "scenario_origin_price_row = price_df[price_df.index >= SCENARIO_AS_OF]\n", + "scenario_origin_price = (\n", + " float(scenario_origin_price_row.iloc[0][\"price\"]) if not scenario_origin_price_row.empty else float(\"nan\")\n", + ")\n", + "\n", + "display(\n", + " Markdown(\n", + " f\"#### Agent response — Stream 3 \"\n", + " f\"*(origin: {SCENARIO_AS_OF.date()}, WTI ${scenario_origin_price:.2f}/bbl)*\\n\\n\"\n", + " f\"Base case: **{scenario_payload.get('base_case', '?')}**\"\n", + " )\n", + ")\n", + "\n", + "base_case = scenario_payload.get(\"base_case\", \"\")\n", + "for s in scenario_payload.get(\"scenarios\", []):\n", + " name = s.get(\"name\", \"?\")\n", + " desc = s.get(\"description\", \"\")\n", + " prob = float(s.get(\"probability\", 0))\n", + " rng = s.get(\"wti_range_60d\", [float(\"nan\"), float(\"nan\")])\n", + " lo_r, hi_r = float(rng[0]), float(rng[1])\n", + " pe = float(s.get(\"point_estimate_60d\", float(\"nan\")))\n", + " drivers = s.get(\"key_drivers\", [])\n", + " base_marker = \" ★ **base case**\" if name == base_case else \"\"\n", + "\n", + " display(\n", + " Markdown(\n", + " f\"---\\n\"\n", + " f\"**{name}**{base_marker}\\n\\n\"\n", + " f\"{desc}\\n\\n\"\n", + " f\"| | |\\n|---|---|\\n\"\n", + " f\"| Probability | **{prob:.0%}** `{prob_bar(prob)}` |\\n\"\n", + " f\"| WTI range (60 days) | ${lo_r:.0f} – ${hi_r:.0f} /bbl |\\n\"\n", + " f\"| Point estimate | **${pe:.0f} /bbl** |\\n\"\n", + " f\"| Key drivers | {' · '.join(drivers) if drivers else '—'} |\\n\"\n", + " )\n", + " )\n", + "\n", + "overall = scenario_payload.get(\"rationale\", \"\")\n", + "if overall:\n", + " display(Markdown(f\"---\\n\\n> **Overall reasoning:** {overall}\"))" + ] }, { "cell_type": "markdown", - "id": "7623eae2785240b9bd12b16a66d81610", + "id": "fd61ab8f", "metadata": {}, "source": [ "---\n", "\n", "## Summary\n", "\n", - "One agent identity (`build_wti_multitask_news_config` / `build_wti_news_config`) with\n", - "three task-specific prompt builders and output schemas demonstrates the bootcamp\n", - "pattern for multi-task agentic forecasting. Continue to\n", - "[`04_systematic_backtest_eval.ipynb`](04_systematic_backtest_eval.ipynb) for the\n", - "stateless backtest harness, then Notebooks 5–6 for the adaptive agent training and\n", - "protected evaluation.\n" + "**One identity, three roles.** The shared `analyst_config` (system instruction +\n", + "`search_web` toolbelt) never changes across streams. Each stream assigns a role\n", + "with an editable **task spec** in the user payload via `WtiMultitaskPromptBuilder`,\n", + "plus a stream-specific `output_schema`.\n", + "\n", + "That is the bootcamp pattern for multi-task agentic forecasting. Notebooks 02/04\n", + "still use a trajectory-specialized system prompt (`build_wti_news_config`) for\n", + "scored backtests — a useful contrast: bake the contract into identity, or keep\n", + "identity stable and swap the user-message ask.\n", + "\n", + "Continue to [`04_systematic_backtest_eval.ipynb`](04_systematic_backtest_eval.ipynb)\n", + "for the stateless backtest harness, then Notebooks 5–6 for the adaptive agent\n", + "training and protected evaluation.\n" ] } ], @@ -252,16 +5121,8 @@ "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.12" + "pygments_lexer": "ipython3" } }, "nbformat": 4, diff --git a/implementations/energy_oil_forecasting/README.md b/implementations/energy_oil_forecasting/README.md index 04a0bc96..0a9eddae 100644 --- a/implementations/energy_oil_forecasting/README.md +++ b/implementations/energy_oil_forecasting/README.md @@ -27,7 +27,7 @@ introduced in notebook 2. |----------|-------|---------| | **[`01_wti_case_study.ipynb`](01_wti_case_study.ipynb)** | **The Case Study Narrative** — rolling Prophet backtest animation, annotated context chart, 2025 vs 2026 coverage punchline, futures curve | No | | **[`02_intro_agentic_predictor.ipynb`](02_intro_agentic_predictor.ipynb)** | **The Agentic Staircase** — 4 capability levels on Mar 2, 2026; inspect configs and prompts | Yes | -| **[`03_one_agent_three_tasks.ipynb`](03_one_agent_three_tasks.ipynb)** | **One Agent, Three Tasks** — trajectory, binary shock, scenario analysis via shared agent identity | Yes | +| **[`03_one_agent_three_tasks.ipynb`](03_one_agent_three_tasks.ipynb)** | **One Agent, Three Tasks** — shared identity once; three editable inline task specs (trajectory, shock, scenario) | Yes | | **[`04_systematic_backtest_eval.ipynb`](04_systematic_backtest_eval.ipynb)** | **Systematic Competition** — 2025 backtest → leaderboard → 2026 protected eval | Yes | ### Adaptive-agent track @@ -62,19 +62,23 @@ Each forecasting origin defines a strict information cutoff (`as_of`). Predictor - **Horizons:** 5, 10, 21 business days - **Output:** Point estimate + standard quantile grid (via `ContinuousAgentForecastOutput`) - **Evaluation:** CRPS and MAE (Notebook 4 backtest) +- **Notebook 03:** editable `TRAJECTORY_TASK_SPEC` in the user payload (same identity as Streams 2–3) ### Task B: Binary Up-shock Probability (Track 1) - **Question:** P(WTI closes > $5/bbl higher in 5 business days) - **Output:** `DiscreteAgentForecastOutput` → `BinaryForecast` - **Evaluation:** Brier score (Notebook 3) +- **Notebook 03:** editable `SHOCK_TASK_SPEC` in the user payload ### Task C: Scenario Analysis (Track 2) -- **Output:** Three scenario cards with probabilities and 60-day ranges +- **Question:** What three scenarios are oil-market analysts debating for WTI over the next 60 days? +- **Output:** Named scenario cards with probabilities, 60-day WTI ranges, point estimates, and key drivers - **Evaluation:** Display / qualitative (Track 2 — not head-to-head scored in backtest) +- **Notebook 03:** editable `SCENARIO_TASK_SPEC` in the user payload -The **one-agent-three-tasks** pattern lives in [`tasks.py`](tasks.py): one `AgentConfig` identity, three `(prompt_builder, output_schema)` pairs via `build_wti_news_predictor(task)`. +The **one-agent-three-tasks** pattern: notebook 03 defines the shared identity once (system instruction + `search_web` toolbelt), then each stream assigns a role with an inline **task spec** via `WtiMultitaskPromptBuilder`. Library defaults live in [`tasks.py`](tasks.py) (`TASK_SPECS` / `build_wti_news_predictor(task)`); notebooks 02/04 keep a trajectory-specialized system prompt (`build_wti_news_config`) for scored trajectory backtests. --- @@ -109,7 +113,7 @@ notebook 05). |-------|--------|------| | Package | `aieng.forecasting.methods.agentic` | `AgentPredictor`, `AgentConfig`, output schema base classes | | Stateless identity | `analyst_agent/agent.py` | Instructions, capability presets, skills — fixed at config time | -| Role per task | `tasks.py` | Prompt builders, `build_wti_news_predictor(task)` | +| Role per task | `tasks.py` + notebook 03 inline specs | `WtiMultitaskPromptBuilder(task_spec=...)`, `build_wti_news_predictor(task)` | | Learning agent | `adaptive_agent/` | Persistent, mutable strategy state updated via self-directed study (notebooks 05–06) | --- diff --git a/implementations/energy_oil_forecasting/analyst_agent/agent.py b/implementations/energy_oil_forecasting/analyst_agent/agent.py index 4956b8e8..043064b0 100644 --- a/implementations/energy_oil_forecasting/analyst_agent/agent.py +++ b/implementations/energy_oil_forecasting/analyst_agent/agent.py @@ -68,6 +68,8 @@ You will receive a JSON payload containing: - `task_spec`: the exact question and required JSON output schema - `as_of`: the forecast origin date (temporal cutoff) +- `horizons`: integer horizon steps (business days ahead) +- `standard_quantiles`: quantile levels for continuous forecasts (when applicable) - `origin_price_usd_bbl`: WTI close on the origin date - `target_history_csv`: compressed WTI daily close history @@ -659,5 +661,10 @@ def build_wti_agent_predictor(config: AgentConfig) -> AgentPredictor: def __getattr__(name: str) -> Any: """Expose ``root_agent`` lazily for schema-free interactive use via ``adk web``.""" if name == "root_agent": - return build_adk_agent(build_wti_basic_config()) + # return build_adk_agent(build_wti_basic_config()) + return build_adk_agent( + build_wti_multitask_news_config( + model=ADVANCED_MODEL, search_model=ADVANCED_MODEL, verifier_model=ADVANCED_MODEL + ) + ) raise AttributeError(f"module {__name__!r} has no attribute {name!r}") diff --git a/implementations/energy_oil_forecasting/tasks.py b/implementations/energy_oil_forecasting/tasks.py index de12da51..1a83e67a 100644 --- a/implementations/energy_oil_forecasting/tasks.py +++ b/implementations/energy_oil_forecasting/tasks.py @@ -13,7 +13,7 @@ import pandas as pd from aieng.forecasting.data.context import ForecastContext -from aieng.forecasting.evaluation.prediction import BinaryForecast, Prediction +from aieng.forecasting.evaluation.prediction import STANDARD_QUANTILES, BinaryForecast, Prediction from aieng.forecasting.evaluation.task import ForecastingTask from aieng.forecasting.methods.agentic import ( AgentPredictor, @@ -24,31 +24,24 @@ from aieng.forecasting.methods.agentic.outputs import AgentForecastOutput from aieng.forecasting.models import LITE_MODEL from energy_oil_forecasting.analyst_agent import ( - WtiPriceForecastPromptBuilder, build_wti_multitask_news_config, - build_wti_news_config, compress_history, ) from energy_oil_forecasting.paths import SHOCK_HORIZON, SHOCK_THRESHOLD from pydantic import BaseModel, Field -# ── Task specification strings (embedded in user prompts for NB3) ─────────── -# Each spec uses the corresponding output class's prompt_schema_json() so the -# required JSON format in the prompt is always in sync with the Pydantic schema. - -TASK_TRAJECTORY_SPEC = ( - "Forecast the WTI crude oil price at the horizons listed in the payload.\n\n" - "If a `set_model_response` tool is available, call it with your complete " - "JSON as `json_response`. Otherwise return the JSON directly as plain text.\n\n" - "Required JSON format:\n" + ContinuousAgentForecastOutput.prompt_schema_json() -) - TaskKind = Literal["trajectory", "shock", "scenario"] class WtiMultitaskPromptBuilder(BaseModel): - """Prompt builder for task-spec-driven agent calls (NB3).""" + """Prompt builder for task-spec-driven agent calls (NB3). + + The system instruction is task-agnostic; the ask lives in ``task_spec``. + The payload also includes ``horizons`` and ``standard_quantiles`` so + trajectory (and any horizon-aware) tasks can read them without baking the + forecasting contract into the system prompt. + """ task_spec: str @@ -61,6 +54,8 @@ def __call__(self, *, task: ForecastingTask, context: ForecastContext) -> str: "task": task.task_id, "task_spec": self.task_spec, "as_of": str(context.as_of)[:10], + "horizons": list(task.horizons), + "standard_quantiles": list(STANDARD_QUANTILES), "origin_price_usd_bbl": float(last_row["value"]), "target_history_csv": compress_history(df), } @@ -155,19 +150,50 @@ def to_predictions( # Task specification strings embedded in user prompts for NB3. # Defined after the output classes so each spec can reference the # corresponding prompt_schema_json() classmethod — single source of truth. +# Notebook 03 copies these into editable cells; the factory uses these defaults. + +TASK_TRAJECTORY_SPEC = ( + "Forecast the WTI crude oil price at each horizon listed in the payload " + "(`horizons`, business days ahead).\n\n" + "Rules:\n" + " - Produce one forecast for each horizon in `horizons`.\n" + " - Use exactly the quantile levels from `standard_quantiles` — " + "no additions, no omissions.\n" + " - `point_forecast` must exactly equal the 0.50 quantile value.\n" + " - Quantile values must be strictly non-decreasing as quantile levels increase.\n" + " - Document your reasoning in the `rationale` fields.\n\n" + "If a `set_model_response` tool is available, call it with your complete " + "JSON as `json_response`. Otherwise return the JSON directly as plain text.\n\n" + "Required JSON format:\n" + ContinuousAgentForecastOutput.prompt_schema_json() +) TASK_SHOCK_SPEC = ( f"Estimate P(up) — the probability that WTI will close MORE THAN\n" f"${int(SHOCK_THRESHOLD)}/bbl HIGHER than today's price at the end of\n" f"{SHOCK_HORIZON} trading days.\n\n" + "This is a directional upside question only.\n\n" + "Calibration guidance:\n" + " - No unusual upside catalyst -> base rate ~10-15%\n" + " - Escalating unconfirmed risk -> 20-40%\n" + " - Confirmed supply disruption -> 60-85%\n\n" "If a `set_model_response` tool is available, call it with your complete " "JSON as `json_response`. Otherwise return the JSON directly as plain text.\n\n" "Required JSON format:\n" + DiscreteAgentForecastOutput.prompt_schema_json() ) TASK_SCENARIOS_SPEC = ( - "Identify the three scenarios oil market analysts are debating for WTI " - "over the next 60 days.\n\n" + "Identify the three scenarios that oil market analysts and experts are most " + "actively debating for WTI crude over the next 60 days, given the current " + "market context and price history.\n\n" + "For each scenario:\n" + " - Give it a concise name (3-6 words)\n" + " - Describe it in 1-2 sentences\n" + " - Assign a probability (all three must sum to <= 1.0)\n" + " - Provide an expected WTI price range at the 60-day horizon as [low, high]\n" + " - Give your point estimate for WTI at 60 days under this scenario\n" + " - List 1-2 key drivers that would cause this scenario to materialise\n\n" + "Also identify which scenario is the base case and provide an overall " + "one-paragraph reasoning summary.\n\n" "If a `set_model_response` tool is available, call it with your complete " "JSON as `json_response`. Otherwise return the JSON directly as plain text.\n\n" "Required JSON format:\n" + ScenarioAgentForecastOutput.prompt_schema_json() @@ -193,6 +219,10 @@ def build_wti_news_predictor( ) -> AgentPredictor: """Build a news-grounded agent predictor for the given task kind. + All three task kinds share the same multitask news identity + (:func:`~energy_oil_forecasting.analyst_agent.build_wti_multitask_news_config`); + only the user-payload ``task_spec`` and output schema change. + Parameters ---------- task : TaskKind @@ -203,12 +233,6 @@ def build_wti_news_predictor( Defaults to the lite model (``"gemini-3.1-flash-lite-preview"``); pass the advanced model (``"gemini-3.5-flash"``) when more capability is needed. """ - if task == "trajectory": - return AgentPredictor( - agent_config=build_wti_news_config(model=model), - prompt_builder=WtiPriceForecastPromptBuilder(), - output_schema=ContinuousAgentForecastOutput, - ) return AgentPredictor( agent_config=build_wti_multitask_news_config(model=model), prompt_builder=WtiMultitaskPromptBuilder(task_spec=TASK_SPECS[task]), @@ -217,13 +241,11 @@ def build_wti_news_predictor( def build_wti_agent_predictor_for_task(config: AgentConfig, task: TaskKind) -> AgentPredictor: - """Wire any WTI agent config to a task-specific predictor.""" - if task == "trajectory": - return AgentPredictor( - agent_config=config, - prompt_builder=WtiPriceForecastPromptBuilder(), - output_schema=ContinuousAgentForecastOutput, - ) + """Wire any WTI agent config to a task-specific predictor. + + Uses the multitask prompt builder for every task kind so the ask rides in + ``task_spec`` rather than in the system instruction. + """ return AgentPredictor( agent_config=config, prompt_builder=WtiMultitaskPromptBuilder(task_spec=TASK_SPECS[task]), diff --git a/implementations/tests/energy_oil_forecasting/test_tasks.py b/implementations/tests/energy_oil_forecasting/test_tasks.py index 4507ac6f..611400cf 100644 --- a/implementations/tests/energy_oil_forecasting/test_tasks.py +++ b/implementations/tests/energy_oil_forecasting/test_tasks.py @@ -8,13 +8,19 @@ from __future__ import annotations +import json +from datetime import datetime +from unittest.mock import MagicMock + +import pandas as pd import pytest +from aieng.forecasting.evaluation.prediction import STANDARD_QUANTILES +from aieng.forecasting.evaluation.task import ForecastingTask from aieng.forecasting.methods.agentic import ( AgentPredictor, ContinuousAgentForecastOutput, DiscreteAgentForecastOutput, ) -from energy_oil_forecasting.analyst_agent import WtiPriceForecastPromptBuilder from energy_oil_forecasting.tasks import ( ScenarioAgentForecastOutput, TaskKind, @@ -26,7 +32,7 @@ @pytest.mark.parametrize( "task, expected_schema, expected_prompt_builder", [ - ("trajectory", ContinuousAgentForecastOutput, WtiPriceForecastPromptBuilder), + ("trajectory", ContinuousAgentForecastOutput, WtiMultitaskPromptBuilder), ("shock", DiscreteAgentForecastOutput, WtiMultitaskPromptBuilder), ("scenario", ScenarioAgentForecastOutput, WtiMultitaskPromptBuilder), ], @@ -51,3 +57,32 @@ def test_build_wti_news_predictor_schema_and_prompt_builder( f"task={task!r}: expected prompt_builder type={expected_prompt_builder.__name__}, " f"got {type(predictor.prompt_builder).__name__}" ) + + +def test_wti_multitask_prompt_builder_includes_horizons_and_quantiles() -> None: + """Payload always carries task_spec, horizons, and the standard quantile grid.""" + builder = WtiMultitaskPromptBuilder(task_spec="Estimate something.") + task = ForecastingTask( + task_id="wti_demo", + target_series_id="CL=F", + horizons=[5, 10, 21], + frequency="B", + description="unit test", + ) + df = pd.DataFrame( + { + "timestamp": pd.to_datetime(["2026-03-01", "2026-03-02"]), + "value": [70.0, 71.0], + } + ) + context = MagicMock() + context.as_of = datetime(2026, 3, 2) + context.get_series.return_value = df + + payload = json.loads(builder(task=task, context=context)) + + assert payload["task_spec"] == "Estimate something." + assert payload["horizons"] == [5, 10, 21] + assert payload["standard_quantiles"] == list(STANDARD_QUANTILES) + assert payload["origin_price_usd_bbl"] == 71.0 + assert "target_history_csv" in payload