Trade & store logistics: per-item market price intelligence + fair-value cross-reference #49

Closed
opened 2026-07-05 01:22:34 +00:00 by coilysiren · 2 comments
Owner

Context

Part of the trade & store logistics push under the DiscordLink-replacement epic (#37), built on the trades-ledger foundation (#6). The fair-price advisor (src/eco_mcp_app/fair_price.py) today maps Eco items to real-world FRED commodities but openly laments it has no in-game price to compare against (see its "Day 3 of Cycle 13 reality" docstring). The CurrencyTrade exporter already ships every individual trade, so the in-game price is derivable now, with no server restart.

No-reset spine

Data comes entirely from data the live server already exports: GET /api/v1/exporter/actions?actionName=CurrencyTrade (columns per #6: BankAccount, Currency, CurrencyAmount, NumberOfItems, BoughtOrSold, ShopOwner, Buyer, Seller, WorldObjectItem, ItemUsed, Citizen, ActionLocation, Count, Time). No new C# mod, no server reset. Reuse the streamed-CSV aggregation pattern in crafting.py, the shifted-column corrector (#5), and the BoughtOrSold 32/33 decode (#6). If #6's normalized trade model has already landed, consume it rather than re-parsing.

Scope

The agent builds a per-item price intelligence layer:

  • Price series per item - unit price = CurrencyAmount / NumberOfItems, per currency, bucketed daily. Median, min, max, and volume (units traded) per bucket.
  • Trend - short-window vs long-window median delta, so an item reads as rising, falling, or flat. Honor per-currency separation (a bucket can be thin).
  • Fair-value cross-reference - wire the in-game median into fair_price.py so the advisor can finally say "in-game median X vs real-world trend Y -> looks over/underpriced," closing the gap its own docstring names. Keep the FRED path working when an item has no in-game trades.
  • Surfaces - a new MCP tool (get_eco_market or an extension of the existing fair_price tool, agent's call) returning text + structured JSON + an _meta.ui card, and a /preview/market.json endpoint for the SPA data plane. The /trade SPA page itself is a sibling issue.

DiscordLink shows only a current asking price with no history and no fair-value context. This exceeds it on the price axis: trend, volume, and a real-world benchmark. Live current-shelf price is the one piece deferred to the reset-gated stores exporter (sibling issue).

Acceptance

  • MCP tool registered, ward exec smoke passes with it.
  • Thin-data and zero-trade paths render without error (early-cycle reality: many items have a handful of trades).
  • respx-mocked unit tests for the price math and the fair-value merge.
  • ward exec test and ward exec lint green.
  • docs/FEATURES.md updated for the new surface.

Dependencies

Foundation: #6. Parent epic: #37. Sibling logistics engine and /trade SPA route consume this.

## Context Part of the trade & store logistics push under the DiscordLink-replacement epic ([#37](https://forgejo.coilysiren.me/coilyco-gaming/eco-app/issues/37)), built on the trades-ledger foundation ([#6](https://forgejo.coilysiren.me/coilyco-gaming/eco-app/issues/6)). The fair-price advisor (`src/eco_mcp_app/fair_price.py`) today maps Eco items to real-world FRED commodities but openly laments it has **no in-game price** to compare against (see its "Day 3 of Cycle 13 reality" docstring). The `CurrencyTrade` exporter already ships every individual trade, so the in-game price is derivable **now, with no server restart**. ## No-reset spine Data comes entirely from data the live server **already exports**: `GET /api/v1/exporter/actions?actionName=CurrencyTrade` (columns per [#6](https://forgejo.coilysiren.me/coilyco-gaming/eco-app/issues/6): `BankAccount, Currency, CurrencyAmount, NumberOfItems, BoughtOrSold, ShopOwner, Buyer, Seller, WorldObjectItem, ItemUsed, Citizen, ActionLocation, Count, Time`). No new C# mod, no server reset. Reuse the streamed-CSV aggregation pattern in `crafting.py`, the shifted-column corrector ([#5](https://forgejo.coilysiren.me/coilyco-gaming/eco-app/issues/5)), and the `BoughtOrSold` 32/33 decode ([#6](https://forgejo.coilysiren.me/coilyco-gaming/eco-app/issues/6)). If [#6](https://forgejo.coilysiren.me/coilyco-gaming/eco-app/issues/6)'s normalized trade model has already landed, consume it rather than re-parsing. ## Scope The agent builds a **per-item price intelligence** layer: * **Price series per item** - unit price = `CurrencyAmount / NumberOfItems`, per currency, bucketed daily. Median, min, max, and volume (units traded) per bucket. * **Trend** - short-window vs long-window median delta, so an item reads as rising, falling, or flat. Honor per-currency separation (a bucket can be thin). * **Fair-value cross-reference** - wire the in-game median into `fair_price.py` so the advisor can finally say "in-game median X vs real-world trend Y -> looks over/underpriced," closing the gap its own docstring names. Keep the FRED path working when an item has no in-game trades. * **Surfaces** - a new MCP tool (`get_eco_market` or an extension of the existing `fair_price` tool, agent's call) returning text + structured JSON + an `_meta.ui` card, and a `/preview/market.json` endpoint for the SPA data plane. The `/trade` SPA page itself is a sibling issue. ## DiscordLink parity DiscordLink shows only a current asking price with no history and no fair-value context. This **exceeds** it on the price axis: trend, volume, and a real-world benchmark. Live current-shelf price is the one piece deferred to the reset-gated stores exporter (sibling issue). ## Acceptance * MCP tool registered, `ward exec smoke` passes with it. * Thin-data and zero-trade paths render without error (early-cycle reality: many items have a handful of trades). * `respx`-mocked unit tests for the price math and the fair-value merge. * `ward exec test` and `ward exec lint` green. * `docs/FEATURES.md` updated for the new surface. ## Dependencies Foundation: [#6](https://forgejo.coilysiren.me/coilyco-gaming/eco-app/issues/6). Parent epic: [#37](https://forgejo.coilysiren.me/coilyco-gaming/eco-app/issues/37). Sibling logistics engine and `/trade` SPA route consume this.
Member

🔒 Reserved by ward agent --driver claude — container engineer-claude-eco-app-49 on host KAI-DESKTOP-TOWER is carrying this issue (reserved 2026-07-05T03:26:22Z). Concurrent ward agent runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); --force overrides.

Do not comment on or edit this issue to steer the run while it is reserved. The engineer seeded the body once at launch and never re-reads it, so a comment or edit reaches only human readers, never the running engineer. A correction goes to a new issue, dispatched fresh — that is the only channel that reaches a run in flight. Where the forge supports it, ward locks this conversation to make that a road-block rather than a convention (ward#494).

— Claude (she/her), via ward agent

<!-- ward-agent-reservation --> 🔒 Reserved by `ward agent --driver claude` — container `engineer-claude-eco-app-49` on host `KAI-DESKTOP-TOWER` is carrying this issue (reserved 2026-07-05T03:26:22Z). Concurrent `ward agent` runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); `--force` overrides. **Do not comment on or edit this issue to steer the run while it is reserved.** The engineer seeded the body once at launch and never re-reads it, so a comment or edit reaches only human readers, never the running engineer. A correction goes to a **new issue, dispatched fresh** — that is the only channel that reaches a run in flight. Where the forge supports it, ward locks this conversation to make that a road-block rather than a convention (ward#494). <!-- ward-agent-signature --> — Claude (she/her), via `ward agent`
Author
Owner

WARD-OUTCOME: done - get_eco_market price-intelligence tool + fair_price in-game cross-reference landed on main (closes #49).

The shape of this one was set by #6 doing most of the heavy lifting already. The trades ledger hands back a clean normalized row model, so the market layer is really just a fold: bucket by (item, currency, day), take medians, and diff a short window against a long one. build_market came out as a pure function over row dicts, which made the price math trivial to unit-test with no HTTP at all — that was the satisfying part.

What fought back a little: the FRED-vs-in-game "over/underpriced" verdict has no shared unit, so I couldn't just compare numbers. I settled on two honest paths — calibration-anchored when a cycle ratio exists, trend-divergence otherwise (real-world climbing while in-game sits flat reads as underpriced). It closes the gap the module's own docstring kept lamenting without pretending dollars and Credits are the same thing. The other snag was keeping the fair_price merge from breaking the FRED-only tests: gating the exporter fetch on an admin key means keyless hosts (and the existing respx suite) never touch it.

Confidence is high — 333 tests green, lint/mypy/pre-commit clean, smoke lists the tool. Merge with #50 (the sibling stores directory) conflicted in the exact files you'd expect and resolved cleanly. Two honest rough edges worth a follow-up: the market series is built over the ledger's newest-4000-row window (fine for thin early-cycle data, but I'd file an issue before late-cycle), and the trend windows (3d/14d, 5% flat band) are guesses tuned for sparse data, not validated against real volume. The /trade SPA page that consumes /preview/market.json is the sibling issue, so this is data-plane-only for now.

WARD-OUTCOME: done - get_eco_market price-intelligence tool + fair_price in-game cross-reference landed on main (closes #49). The shape of this one was set by #6 doing most of the heavy lifting already. The trades ledger hands back a clean normalized row model, so the market layer is really just a fold: bucket by (item, currency, day), take medians, and diff a short window against a long one. `build_market` came out as a pure function over row dicts, which made the price math trivial to unit-test with no HTTP at all — that was the satisfying part. What fought back a little: the FRED-vs-in-game "over/underpriced" verdict has no shared unit, so I couldn't just compare numbers. I settled on two honest paths — calibration-anchored when a cycle ratio exists, trend-divergence otherwise (real-world climbing while in-game sits flat reads as underpriced). It closes the gap the module's own docstring kept lamenting without pretending dollars and Credits are the same thing. The other snag was keeping the fair_price merge from breaking the FRED-only tests: gating the exporter fetch on an admin key means keyless hosts (and the existing respx suite) never touch it. Confidence is high — 333 tests green, lint/mypy/pre-commit clean, smoke lists the tool. Merge with #50 (the sibling stores directory) conflicted in the exact files you'd expect and resolved cleanly. Two honest rough edges worth a follow-up: the market series is built over the ledger's newest-4000-row window (fine for thin early-cycle data, but I'd file an issue before late-cycle), and the trend windows (3d/14d, 5% flat band) are guesses tuned for sparse data, not validated against real volume. The `/trade` SPA page that consumes `/preview/market.json` is the sibling issue, so this is data-plane-only for now.
Sign in to join this conversation.
No description provided.