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:
+95
-14
@@ -103,41 +103,122 @@ def latest_prices(
|
||||
str | None,
|
||||
Field(description="Filter to one delivery label (e.g. 'May 2026', 'Oct/Nov 2026')."),
|
||||
] = None,
|
||||
kind: Annotated[
|
||||
str | None,
|
||||
Field(description="Filter to one commodity kind: 'grain' or 'fertilizer'. Omit for all."),
|
||||
] = None,
|
||||
) -> str:
|
||||
"""Snapshot of the latest scraped bid per (source, commodity, delivery)."""
|
||||
"""Snapshot of the latest scraped bid per (source, commodity, delivery).
|
||||
|
||||
Every filter is optional and AND'd together — pivot by elevator, crop,
|
||||
delivery, or kind in any combination."""
|
||||
cm = commodity.strip().lower() if commodity else None
|
||||
with track("latest_prices", commodity=cm, source=source, delivery=delivery):
|
||||
payload = client.latest(commodity=cm, source=source, delivery=delivery)
|
||||
with track("latest_prices", commodity=cm, source=source, delivery=delivery, kind=kind):
|
||||
payload = client.latest(commodity=cm, source=source, delivery=delivery, kind=kind)
|
||||
return fmt.fmt_latest(payload)
|
||||
|
||||
|
||||
def _filter_source(payload: dict, source: str | None) -> dict:
|
||||
"""Narrow a history payload's rows to one source display name, in place."""
|
||||
if source:
|
||||
payload["rows"] = [
|
||||
r for r in payload.get("rows") or [] if r.get("source_name") == source
|
||||
]
|
||||
return payload
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def price_history(
|
||||
commodity: Annotated[
|
||||
str, Field(description="One of corn / soy / wheat / map / potash / lime.")
|
||||
],
|
||||
str | None,
|
||||
Field(description="Filter to one crop (corn / soy / wheat / map / potash / "
|
||||
"lime). Omit to span every crop."),
|
||||
] = None,
|
||||
source: Annotated[
|
||||
str | None,
|
||||
Field(description="Optional source display name to narrow the chart."),
|
||||
Field(description="Filter to one elevator by exact display name. Omit for all."),
|
||||
] = None,
|
||||
delivery: Annotated[
|
||||
str | None,
|
||||
Field(description="Optional delivery label to narrow the chart."),
|
||||
Field(description="Filter to one delivery label (e.g. 'Jul 2026'). Omit for all."),
|
||||
] = None,
|
||||
days: Annotated[
|
||||
int, Field(ge=1, le=365, description="Lookback window in days.")
|
||||
] = 30,
|
||||
) -> str:
|
||||
"""Compact price history per (source, delivery) for the chosen commodity.
|
||||
"""Price history per (elevator, crop, delivery), with every filter optional.
|
||||
|
||||
Returns per-series ▲/▼ trend annotations plus the raw points if the
|
||||
window has fewer than ~60 samples."""
|
||||
cm = commodity.strip().lower()
|
||||
Pivot it however you like — one crop at one elevator, every elevator for one
|
||||
crop, one delivery period across all crops, etc. Each series gets a ▲/▼ bid
|
||||
trend plus its basis first→last; the raw bid/basis/futures points are
|
||||
included when the window has fewer than ~60 samples."""
|
||||
cm = commodity.strip().lower() if commodity else None
|
||||
with track("price_history", commodity=cm, source=source, delivery=delivery, days=days):
|
||||
payload = client.history(commodity=cm, delivery=delivery, days=days)
|
||||
if source:
|
||||
payload["rows"] = [r for r in payload.get("rows") or [] if r.get("source_name") == source]
|
||||
return fmt.fmt_history(payload)
|
||||
return fmt.fmt_history(_filter_source(payload, source))
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def basis_movement(
|
||||
commodity: Annotated[
|
||||
str | None,
|
||||
Field(description="Filter to one grain (corn / soy / wheat). Omit to span all grains."),
|
||||
] = None,
|
||||
source: Annotated[
|
||||
str | None,
|
||||
Field(description="Filter to one elevator by exact display name. Omit for all."),
|
||||
] = None,
|
||||
delivery: Annotated[
|
||||
str | None,
|
||||
Field(description="Filter to one delivery label (e.g. 'Jul 2026'). Omit for all."),
|
||||
] = None,
|
||||
days: Annotated[
|
||||
int, Field(ge=1, le=365, description="Lookback window in days.")
|
||||
] = 30,
|
||||
) -> str:
|
||||
"""Aggregated basis trend — one headline line per crop (the cheap overview).
|
||||
|
||||
Rolls every matching elevator/delivery series up to a single average basis
|
||||
first→last per crop, with how far it moved (positive = cash strengthened vs
|
||||
futures). Use this for 'how is basis moving overall', optionally narrowed to
|
||||
a crop and/or elevator. For the per-elevator breakdown use basis_detail."""
|
||||
cm = commodity.strip().lower() if commodity else None
|
||||
with track("basis_movement", commodity=cm, source=source, delivery=delivery, days=days):
|
||||
if cm is not None and cm not in VALID_GRAIN:
|
||||
return f"`commodity` must be one of {sorted(VALID_GRAIN)} (or omit for all grains)"
|
||||
payload = client.history(commodity=cm, delivery=delivery, days=days)
|
||||
return fmt.fmt_basis_movement(_filter_source(payload, source))
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
def basis_detail(
|
||||
commodity: Annotated[
|
||||
str | None,
|
||||
Field(description="Filter to one grain (corn / soy / wheat). Omit to span all grains."),
|
||||
] = None,
|
||||
source: Annotated[
|
||||
str | None,
|
||||
Field(description="Filter to one elevator by exact display name. Omit for all."),
|
||||
] = None,
|
||||
delivery: Annotated[
|
||||
str | None,
|
||||
Field(description="Filter to one delivery label (e.g. 'Jul 2026'). Omit for all."),
|
||||
] = None,
|
||||
days: Annotated[
|
||||
int, Field(ge=1, le=365, description="Lookback window in days.")
|
||||
] = 30,
|
||||
) -> str:
|
||||
"""Per-(elevator, crop, delivery) basis trend — the drill-down for basis_movement.
|
||||
|
||||
One compact row per series: basis first→last over the window and how far it
|
||||
moved. All filters optional, so you can scope to one crop, one elevator, one
|
||||
delivery, or any combination."""
|
||||
cm = commodity.strip().lower() if commodity else None
|
||||
with track("basis_detail", commodity=cm, source=source, delivery=delivery, days=days):
|
||||
if cm is not None and cm not in VALID_GRAIN:
|
||||
return f"`commodity` must be one of {sorted(VALID_GRAIN)} (or omit for all grains)"
|
||||
payload = client.history(commodity=cm, delivery=delivery, days=days)
|
||||
return fmt.fmt_basis_detail(_filter_source(payload, source))
|
||||
|
||||
|
||||
@mcp.tool()
|
||||
|
||||
Reference in New Issue
Block a user