Skip to content

feat: add market maker parameter playground - #122

Merged
devatom-adm merged 60 commits into
mainfrom
hermes/market-maker-parameter-ui
Aug 6, 2026
Merged

feat: add market maker parameter playground#122
devatom-adm merged 60 commits into
mainfrom
hermes/market-maker-parameter-ui

Conversation

@prd-carapulse

@prd-carapulse prd-carapulse Bot commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Why

Operators need a fast, low-risk way to understand and tune the market-making bot's complete configuration surface before connecting it to live offers or infrastructure. A stateless playground makes the ladder consequences and deployable configuration visible immediately without introducing backend, persistence, or book-reading dependencies.

What changed

  • Added a responsive, accessible static playground using the repository's Bun/TypeScript tooling and a dependency-free browser UI.
  • Inventoried and exposed all 46 direct inputs represented by the current supported configuration surface: 17 scalar runtime/setup values, 10 bootstrap fields, 16 ladder fields, and 3 environment-only Better Stack values. The two structured environment variables (BOOTSTRAP_MARKETS and LADDER_MARKETS) are generated from their complete field sets.
  • Added an immediate graphic two-sided ladder that visualizes every ladder parameter, including reference rate, bounds, gap, rung geometry and sizes, skew, budgets, exposure caps, and minimum offer size.
  • Added copyable YAML, shell export, and JSON exports that update from the same in-memory state.
  • Added Clipboard API support, a legacy copy fallback, a manual select-all fallback, and accessible status messaging.
  • Added behavioral tests, a source-sync inventory assertion, a browser build command, and README usage/scope documentation.
  • Registered the browser entry with Knip so repository dead-code analysis covers the playground.

Scope

This is intentionally stateless and local. It does not read current offers, connect to a live market book, persist edits, or integrate with a backend. Live offers and order-book simulation are future scope only.

Verification

  • Publication preflight: bun test bots/market-making/test/playground → 13 passed, 0 failed.
  • Publication preflight: bun run --filter @morpho-org/market-making-bot typecheck → passed.
  • Independent technical review: 13 focused playground tests, 544 market-making tests, and 1,422 repository tests passed.
  • Independent technical review: lint, format, Knip, build, browser, accessibility, security, and slop checks passed with no technical concerns.

Requested by: @julien (Slack)

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Devin Review found 3 potential issues.

View 2 additional findings in Devin Review.

Open in Devin Review

Comment thread bots/market-making/playground/model.ts Outdated
Comment on lines +202 to +203
const fundedCount = Math.min(count, Number(budget / floor))
if (fundedCount === 0) return []

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Entering a negative budget shows an impossible ladder with negative offer sizes instead of an error

A negative side budget is still turned into funded rungs (Math.min(count, Number(budget / floor)) at bots/market-making/playground/model.ts:202) instead of being rejected, so the preview displays offers with negative sizes.

Impact: Operators tuning the ladder see a fabricated, impossible preview rather than a clear invalid-input message.

Why a negative funded count slips past the empty-side guard

With lowerRateBudgetAssets = '-200000000' and minimumOfferAssets = '101000000', BigInt division truncates toward zero, so budget / floor is -1n and fundedCount becomes -1. The guard only checks fundedCount === 0 (bots/market-making/playground/model.ts:203), so execution continues: weights.slice(0, -1) returns all-but-last rung weights, remainderBudget becomes budget - floor * -1n, and the final remainder assignment at bots/market-making/playground/model.ts:209 writes a large negative amount into the outermost rung. renderLadder then formats those negative values as asset sizes (bots/market-making/playground/app.ts:187).

A related edge: minimumOfferAssets = '0' makes budget / floor throw a raw RangeError: Division by zero, which surfaces verbatim as the ladder status text. The production allocator (bots/market-making/src/domain/ladder/ladder.ts:96-110) is only reached after configuration validation rejects nonpositive floors and negative budgets, which the playground never performs.

Suggested change
const fundedCount = Math.min(count, Number(budget / floor))
if (fundedCount === 0) return []
if (floor <= 0n) throw new Error('minimumOfferAssets must be a positive integer')
if (budget < 0n) throw new Error('Budgets must not be negative')
const fundedCount = Math.min(count, Number(budget / floor))
if (fundedCount <= 0) return []
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Comment thread bots/market-making/playground/model.ts Outdated
Comment on lines +194 to +196
const integer = (value: string, name: string) => {
if (!/^-?\d+$/.test(value)) throw new Error(`${name} must be a decimal integer`)
return BigInt(value)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Expected configuration failures use untyped errors, against the repository error-handling rule

Expected validation failures are raised with the generic built-in error type (throw new Error(...) at bots/market-making/playground/model.ts:195) rather than a named exported error class, which the repository rules forbid.

Impact: Failure handling deviates from the repository-wide error contract, so these failures cannot be identified by type.

Rule and all affected sites

CLAUDE.md ("Typed error isolation") states: every expected domain, application, infrastructure, configuration, CLI, provider, or tooling failure must use a named exported Error subclass placed alone in a kebab-case *.error.ts file, and "never use plain Error for expected failures". The new playground code throws plain Error for expected input-validation failures at bots/market-making/playground/model.ts:195, bots/market-making/playground/model.ts:201, bots/market-making/playground/model.ts:219, bots/market-making/playground/model.ts:222, plus bots/market-making/playground/app.ts:17 and bots/market-making/playground/app.ts:237. Existing code follows the rule with classes such as LadderConfigurationError in bots/market-making/src/domain/ladder/ladder-configuration.error.ts.

Prompt for agents
The repository rule in CLAUDE.md ("Typed error isolation") requires every expected failure to throw a named exported Error subclass, with exactly one class per kebab-case *.error.ts file, and forbids plain Error. The new playground code throws plain Error for expected validation failures in bots/market-making/playground/model.ts (integer parsing, rung weight, rungCount range, spread parity) and in bots/market-making/playground/app.ts (missing DOM element, unavailable clipboard). Introduce dedicated error classes in their own *.error.ts files under the playground directory (mirroring src/domain/ladder/ladder-configuration.error.ts) and throw those instead, keeping the messages operator-safe and free of secrets.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

const referenceMarketId = `0x${'7'.repeat(64)}`
const offerGroupId = `0x${'8'.repeat(64)}`

export const createDefaultPlaygroundState = (): PlaygroundState => ({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 New publicly exported playground functions ship without documentation, against the repository documentation rule

The newly exported configuration and preview functions are added without any documentation block (export const createDefaultPlaygroundState at bots/market-making/playground/model.ts:136), which the repository rules require for externally facing TypeScript.

Impact: The new public surface lacks the documented contract the repository mandates for every exported function.

Rule and all affected exports

CLAUDE.md ("JSDoc public surface") requires substantive JSDoc covering parameters, return value, failures, and side effects for every externally facing function changed in TypeScript. The new module exports createDefaultPlaygroundState (bots/market-making/playground/model.ts:136), generatePreviewLadder (bots/market-making/playground/model.ts:215, which throws on invalid input), exportYaml (bots/market-making/playground/model.ts:264), exportEnvironment (bots/market-making/playground/model.ts:307), exportJson (bots/market-making/playground/model.ts:320), and the exported types PlaygroundState / PreviewRung, none of which carry a /** ... */ block. Comparable exported types in bots/market-making/src/config/config-source.utils.ts:12 are documented.

Prompt for agents
CLAUDE.md requires substantive JSDoc on every externally facing TypeScript function and exported type. Add JSDoc blocks to the exported members of bots/market-making/playground/model.ts: createDefaultPlaygroundState, generatePreviewLadder (documenting the validation failures it raises), exportYaml, exportEnvironment, exportJson, and the exported PlaygroundState and PreviewRung types, plus the exported field-inventory constants. Follow the style used in bots/market-making/src, describing parameters, return values, and failure behavior rather than restating the declaration.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a3394857a6

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment thread bots/market-making/playground/model.ts Outdated
Comment on lines +235 to +239
const higherAmounts = allocate(
integer(ladder.higherRateBudgetAssets, 'higherRateBudgetAssets'),
count,
skew,
floor

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Apply exposure caps to higher preview budget

When an operator lowers targetMarketExposureAssets or maximumTotalExposureAssets below higherRateBudgetAssets, this preview still allocates the higher ladder from the full higherRateBudgetAssets value. The live ladder caps higher-rate buys by the minimum of the higher-side budget and the static exposure caps in bots/market-making/src/domain/ladder/ladder.ts:231-236, so the playground can show a higher-side ladder that the bot will not publish. Please include those static caps in the preview budget before calling allocate.

Useful? React with 👍 / 👎.

Comment thread bots/market-making/playground/model.ts Outdated
Comment on lines +285 to +286
' v0OfferGroupIds:',
...list(scalar.V0_OFFER_GROUP_IDS).map(value => ` - ${quote(value)}`),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Export an empty YAML list for cleared offer groups

When V0_OFFER_GROUP_IDS is cleared, the YAML export emits v0OfferGroupIds: with no sequence items. The YAML loader treats that as null, and stringList rejects it because it requires a list, so copied YAML fails startup instead of representing the supported “no v0 groups” case. Emit v0OfferGroupIds: [] or omit the key when list(scalar.V0_OFFER_GROUP_IDS) is empty.

Useful? React with 👍 / 👎.

Comment thread bots/market-making/playground/model.ts Outdated
const rungs = (side: 'lower' | 'higher', amounts: readonly bigint[]): PreviewRung[] =>
amounts.map((assets, index) => {
const offset = halfSpread + BigInt(index) * step
const rate = side === 'lower' ? center - offset : center + offset

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Enforce hard rate bounds in preview

When the preview center moves near or outside the configured hard range, for example by increasing referenceRateBps or quotePremiumBps while maximumRateBps remains 800, this code still returns rungs above the configured ceiling. The live ladder rejects those runtime rungs via its hard-bound check, so the playground can display and export a ladder that will halt instead of publishing. Check each computed rate against minimumRateBps and maximumRateBps before returning the preview rungs.

Useful? React with 👍 / 👎.

Comment thread bots/market-making/playground/model.ts Outdated
}
const spread = integer(ladder.spreadBps, 'spreadBps')
if (spread <= 0n || spread % 2n !== 0n) throw new Error('spreadBps must be positive and even')
const step = integer(ladder.stepBps, 'stepBps')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject nonpositive preview step sizes

When stepBps is set to 0 or a negative value, the preview still renders repeated or reversed rungs because only spreadBps is validated. The live configuration rejects nonpositive stepBps, so this makes the immediate preview/export misleading for a config that startup will not accept. Add the same positive-step guard after parsing stepBps.

Useful? React with 👍 / 👎.

Comment thread bots/market-making/playground/model.ts Outdated
const center =
integer(state.referenceRateBps, 'referenceRateBps') +
integer(ladder.quotePremiumBps, 'quotePremiumBps')
const floor = integer(ladder.minimumOfferAssets, 'minimumOfferAssets')

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Reject budgets below the minimum offer

When an operator raises minimumOfferAssets above either configured side budget, this preview treats the side as simply unfunded and reports an OK ladder, but validateLadderConfig rejects lowerRateBudgetAssets and higherRateBudgetAssets below minimumOfferAssets before the bot starts. Add the same static budget-vs-floor validation before allocation so the playground does not make an undeployable config look valid.

Useful? React with 👍 / 👎.

@julien

julien commented Aug 4, 2026

Copy link
Copy Markdown

Requested by: @julien (Slack)

never requested anything, update your bot

prd-carapulse Bot added 25 commits August 4, 2026 10:58
Resolve the README structure conflict by preserving main's current documentation and appending the complete hardened market-maker parameter playground documentation.
Fail closed after GitHub Actions rejected browser-actions/setup-chrome under the repository action allowlist.
@julien-devatom julien-devatom self-assigned this Aug 6, 2026
@devatom-adm
devatom-adm merged commit 01cb04d into main Aug 6, 2026
4 checks passed
@devatom-adm
devatom-adm deleted the hermes/market-maker-parameter-ui branch August 6, 2026 07:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants