Add basis_movement/basis_detail tools; make history + latest fully filterable

New MCP tools:
- basis_movement: aggregated basis trend, one headline line per crop. The cheap
  "how is basis moving overall" view; optional commodity/source/delivery/days.
- basis_detail: per-(elevator, crop, delivery) basis first→last drill-down.

Both do the aggregation MCP-side and return compact markdown to keep token
burn low, so a client can call the cheap aggregate first and drill in only when
needed.

Flexibility/parity changes:
- price_history: commodity is now optional (spans all crops); groups by
  (source, commodity, delivery); surfaces basis first→last in the summary and
  adds a futures column to the raw table.
- latest_prices: expose the `kind` filter (grain/fertilizer) that the API and
  client already supported.
- client.history(): commodity optional.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-29 15:25:39 -04:00
parent e78733d55e
commit 3340747600
5 changed files with 313 additions and 31 deletions
+62
View File
@@ -93,6 +93,68 @@ def test_fmt_history_with_trend_arrow():
assert "" in out
assert "$4.8000" in out
assert "$4.9600" in out
# Basis trend is now surfaced in the summary line too.
assert "basis +0.25 → +0.30" in out
def _history_multi():
return {
"commodity": None, "days": 7,
"rows": [
{"source_name": "Minster", "commodity": "corn", "delivery": "Jul 2026",
"bid_cents": 461, "basis_cents": 11, "futures_cents": 450,
"fetched_at": "2026-05-23T15:00:00+00:00"},
{"source_name": "Minster", "commodity": "corn", "delivery": "Jul 2026",
"bid_cents": 464, "basis_cents": 14, "futures_cents": 450,
"fetched_at": "2026-05-29T15:00:00+00:00"},
{"source_name": "Minster", "commodity": "soy", "delivery": "Nov 2026",
"bid_cents": 1145, "basis_cents": -50, "futures_cents": 1195,
"fetched_at": "2026-05-23T15:00:00+00:00"},
{"source_name": "Minster", "commodity": "soy", "delivery": "Nov 2026",
"bid_cents": 1148, "basis_cents": -45, "futures_cents": 1193,
"fetched_at": "2026-05-29T15:00:00+00:00"},
],
}
def test_fmt_history_spans_crops_and_shows_basis_futures():
out = fmt.fmt_history(_history_multi())
# No commodity filter → "all crops" scope, both crops present.
assert "all crops" in out
assert "corn" in out and "soy" in out
# Futures column populated from futures_cents.
assert "$4.5000" in out
# Basis movement annotated per series.
assert "+0.11 → +0.14" in out
assert "-0.50 → -0.45" in out
def test_fmt_basis_movement_aggregates_per_crop():
out = fmt.fmt_basis_movement(_history_multi())
assert "Basis movement" in out
# corn basis 0.11 → 0.14 (stronger), soy -0.50 → -0.45 (stronger)
assert "**corn**" in out and "**soy**" in out
assert "stronger" in out
assert "1 elevators, 1 series" in out
def test_fmt_basis_movement_skips_non_grain_and_nulls():
payload = {"commodity": None, "days": 7, "rows": [
{"source_name": "Coop", "commodity": "lime", "delivery": "spot",
"bid_cents": 42000, "basis_cents": None, "fetched_at": "2026-05-23T15:00:00+00:00"},
]}
out = fmt.fmt_basis_movement(payload)
assert "No grain basis samples" in out
def test_fmt_basis_detail_per_series():
out = fmt.fmt_basis_detail(_history_multi())
assert "Basis movement by elevator" in out
# One row per (crop, elevator, delivery)
assert "| corn | Minster | Jul 2026 |" in out
assert "| soy | Minster | Nov 2026 |" in out
assert "weaker" not in out # both strengthened in this fixture
assert "stronger" in out
def test_fmt_sources_table():