feat: add market maker parameter playground - #122
Conversation
| const fundedCount = Math.min(count, Number(budget / floor)) | ||
| if (fundedCount === 0) return [] |
There was a problem hiding this comment.
🟡 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.
| 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 [] |
Was this helpful? React with 👍 or 👎 to provide feedback.
| const integer = (value: string, name: string) => { | ||
| if (!/^-?\d+$/.test(value)) throw new Error(`${name} must be a decimal integer`) | ||
| return BigInt(value) |
There was a problem hiding this comment.
🟡 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.
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 => ({ |
There was a problem hiding this comment.
🟡 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
💡 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".
| const higherAmounts = allocate( | ||
| integer(ladder.higherRateBudgetAssets, 'higherRateBudgetAssets'), | ||
| count, | ||
| skew, | ||
| floor |
There was a problem hiding this comment.
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 👍 / 👎.
| ' v0OfferGroupIds:', | ||
| ...list(scalar.V0_OFFER_GROUP_IDS).map(value => ` - ${quote(value)}`), |
There was a problem hiding this comment.
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 👍 / 👎.
| 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 |
There was a problem hiding this comment.
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 👍 / 👎.
| } | ||
| 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') |
There was a problem hiding this comment.
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 👍 / 👎.
| const center = | ||
| integer(state.referenceRateBps, 'referenceRateBps') + | ||
| integer(ladder.quotePremiumBps, 'quotePremiumBps') | ||
| const floor = integer(ladder.minimumOfferAssets, 'minimumOfferAssets') |
There was a problem hiding this comment.
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 👍 / 👎.
never requested anything, update your bot |
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.
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
BOOTSTRAP_MARKETSandLADDER_MARKETS) are generated from their complete field sets.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
bun test bots/market-making/test/playground→ 13 passed, 0 failed.bun run --filter @morpho-org/market-making-bot typecheck→ passed.Requested by: @julien (Slack)