Trade & store logistics engine: cheapest-source, arbitrage spreads, supply-gap finder #51

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

Context

The core trade & store logistics deliverable under the DiscordLink-replacement epic (#37). This is the "logistics" Kai is focused on: not just showing trades, but telling a player where to buy, where to sell, and what to haul. Builds on the price intelligence and store/trader directory siblings, both on the trades-ledger foundation (#6).

No-reset spine

Consumes the already-exported CurrencyTrade history via the sibling price and directory modules. No new C# mod, no server reset. Where a computation wants the current shelf (live asking price), it degrades to the recent-median from history and marks the value as history-derived, so it works today and sharpens automatically when the reset-gated stores exporter (sibling) lands.

Scope

The agent builds a logistics engine MCP tool (find_eco_trade or similar) answering:

  • Cheapest source - for an item, rank stores/sellers by recent effective price. "Where is iron ingot cheapest right now."
  • Best resale - rank buyers/sell-side by price for a player holding stock. "Where do I get the most for my boards."
  • Arbitrage / spread - detect items where a buy price at one store is below a sell price at another by more than a threshold, ranked by spread and volume. The buy-low-sell-high board.
  • Supply gap / demand - items with active buy orders (demand) but thin or absent sell-side supply, and items priced well above their in-game median. What a new store should stock.
  • Surfaces - text + structured JSON + _meta.ui card, and /preview/logistics.json for the SPA. Each result names the store, owner, price, and freshness so a player can act on it.

This has no DiscordLink equivalent - it only lists trades, it never reasons over them. This is the clearest "exceed" in the whole push: turning the trade ledger into buy/sell/haul decisions.

Acceptance

  • MCP tool registered, ward exec smoke passes.
  • Thresholds are named constants with sane seeds and a short comment on tuning.
  • Empty and single-store paths return an honest "not enough market depth" rather than a misleading spread.
  • respx-mocked unit tests for cheapest-source, spread detection, and supply-gap.
  • ward exec test / ward exec lint green. docs/FEATURES.md updated.

Dependencies

Consumes the price-intelligence and store/trader-directory siblings (both on #6). If they have not landed, build against their documented /preview/*.json contract and integrate. Parent: #37.

## Context The **core trade & store logistics** deliverable under the DiscordLink-replacement epic ([#37](https://forgejo.coilysiren.me/coilyco-gaming/eco-app/issues/37)). This is the "logistics" Kai is focused on: not just showing trades, but telling a player **where to buy, where to sell, and what to haul**. Builds on the price intelligence and store/trader directory siblings, both on the trades-ledger foundation ([#6](https://forgejo.coilysiren.me/coilyco-gaming/eco-app/issues/6)). ## No-reset spine Consumes the already-exported `CurrencyTrade` history via the sibling price and directory modules. No new C# mod, no server reset. Where a computation wants the current shelf (live asking price), it degrades to the recent-median from history and marks the value as history-derived, so it works today and sharpens automatically when the reset-gated stores exporter (sibling) lands. ## Scope The agent builds a **logistics engine** MCP tool (`find_eco_trade` or similar) answering: * **Cheapest source** - for an item, rank stores/sellers by recent effective price. "Where is iron ingot cheapest right now." * **Best resale** - rank buyers/sell-side by price for a player holding stock. "Where do I get the most for my boards." * **Arbitrage / spread** - detect items where a buy price at one store is below a sell price at another by more than a threshold, ranked by spread and volume. The buy-low-sell-high board. * **Supply gap / demand** - items with active buy orders (demand) but thin or absent sell-side supply, and items priced well above their in-game median. What a new store should stock. * **Surfaces** - text + structured JSON + `_meta.ui` card, and `/preview/logistics.json` for the SPA. Each result names the store, owner, price, and freshness so a player can act on it. ## DiscordLink parity This has **no DiscordLink equivalent** - it only lists trades, it never reasons over them. This is the clearest "exceed" in the whole push: turning the trade ledger into buy/sell/haul decisions. ## Acceptance * MCP tool registered, `ward exec smoke` passes. * Thresholds are named constants with sane seeds and a short comment on tuning. * Empty and single-store paths return an honest "not enough market depth" rather than a misleading spread. * `respx`-mocked unit tests for cheapest-source, spread detection, and supply-gap. * `ward exec test` / `ward exec lint` green. `docs/FEATURES.md` updated. ## Dependencies Consumes the price-intelligence and store/trader-directory siblings (both on [#6](https://forgejo.coilysiren.me/coilyco-gaming/eco-app/issues/6)). If they have not landed, build against their documented `/preview/*.json` contract and integrate. Parent: [#37](https://forgejo.coilysiren.me/coilyco-gaming/eco-app/issues/37).
Author
Owner

The live store-offer exporter mod landed in #55 (`mods/stores`, `GET /api/v1/stores`). The offer-level fields this engine needs for cheapest-source / arbitrage / supply-gap (`item`, `buying`, `price`, `quantity` per store) are documented here: mods/stores/docs/dto.md. The C# shell harness serves the same shape on :5101 (ward exec run-shell-stores), so the live path is wireable offline ahead of the deploy-time restart.

The live store-offer exporter mod landed in #55 (\`mods/stores\`, \`GET /api/v1/stores\`). The offer-level fields this engine needs for cheapest-source / arbitrage / supply-gap (\`item\`, \`buying\`, \`price\`, \`quantity\` per store) are documented here: [mods/stores/docs/dto.md](https://forgejo.coilysiren.me/coilyco-gaming/eco-app/src/branch/main/mods/stores/docs/dto.md). The C# shell harness serves the same shape on :5101 (`ward exec run-shell-stores`), so the live path is wireable offline ahead of the deploy-time restart.
Member

🔒 Reserved by ward agent --driver claude — container engineer-claude-eco-app-51 on host KAI-DESKTOP-TOWER is carrying this issue (reserved 2026-07-05T04:02:50Z). 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-51` on host `KAI-DESKTOP-TOWER` is carrying this issue (reserved 2026-07-05T04:02:50Z). 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 - find_eco_trade logistics engine (cheapest-source / best-resale / arbitrage / supply-gap) landed on main with respx tests, /preview/logistics.json, and an MCP card.

Retrospective: this one went smoothly because the two siblings (#49 market, #50 stores) had already carved the pattern deep - a normalized fold + a pure builder + respx wiring + a Jinja card - so most of the work was picking the right data model and letting the existing fetch_parsed_trades spine carry the network. The design call that mattered was collapsing both inputs (live /api/v1/stores shelf and the history reconstruction) into one ShelfOffer unit so the four boards are a single pure function over a list - that made the acceptance-critical "honest empty / single-store" cases fall out naturally rather than needing special-casing.

What fought back a little: getting the buy/sell perspective straight took care. The DTO's buying is the store's point of view, and arbitrage flips it again (you buy from a store's sell offer, sell to a store's buy offer), so I leaned on explicit tests to keep the direction honest. Nothing surprising otherwise - lint's ambiguous-unicode check caught a × and an em-dash I'd have missed.

Confidence is high on the engine logic (well-covered, pure, deterministic) and the history path (exercised end-to-end against mocked exporter CSVs). The one genuinely unexercised-in-production surface is the live shelf - I tested it against the documented DTO and the C# shell harness shape, but the real reset-gated mod hasn't deployed, so first contact with live data is the thing to watch. The #54 SPA surface merged cleanly and already consumes this endpoint, so the two halves met in the middle as intended.

Rough edges / follow-ups worth filing: (1) the thresholds are seeded, not tuned - once real early-cycle shelves exist they'll want a pass against actual spread noise; (2) the in-game median that powers the "over-priced" gap signal is computed per-fetch rather than reusing market.py's series, so there's a small dedup opportunity if the two ever want one median source of truth.

WARD-OUTCOME: done - find_eco_trade logistics engine (cheapest-source / best-resale / arbitrage / supply-gap) landed on main with respx tests, /preview/logistics.json, and an MCP card. Retrospective: this one went smoothly because the two siblings (#49 market, #50 stores) had already carved the pattern deep - a normalized fold + a pure builder + respx wiring + a Jinja card - so most of the work was picking the right data model and letting the existing `fetch_parsed_trades` spine carry the network. The design call that mattered was collapsing both inputs (live `/api/v1/stores` shelf and the history reconstruction) into one `ShelfOffer` unit so the four boards are a single pure function over a list - that made the acceptance-critical "honest empty / single-store" cases fall out naturally rather than needing special-casing. What fought back a little: getting the buy/sell *perspective* straight took care. The DTO's `buying` is the store's point of view, and arbitrage flips it again (you buy from a store's *sell* offer, sell to a store's *buy* offer), so I leaned on explicit tests to keep the direction honest. Nothing surprising otherwise - lint's ambiguous-unicode check caught a `×` and an em-dash I'd have missed. Confidence is high on the engine logic (well-covered, pure, deterministic) and the history path (exercised end-to-end against mocked exporter CSVs). The one genuinely unexercised-in-production surface is the *live* shelf - I tested it against the documented DTO and the C# shell harness shape, but the real reset-gated mod hasn't deployed, so first contact with live data is the thing to watch. The `#54` SPA surface merged cleanly and already consumes this endpoint, so the two halves met in the middle as intended. Rough edges / follow-ups worth filing: (1) the thresholds are seeded, not tuned - once real early-cycle shelves exist they'll want a pass against actual spread noise; (2) the in-game median that powers the "over-priced" gap signal is computed per-fetch rather than reusing `market.py`'s series, so there's a small dedup opportunity if the two ever want one median source of truth.
Sign in to join this conversation.
No description provided.