Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions bots/market-making/.env.example
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Environment values override the corresponding YAML values.
CHAIN_ID=8453
RPC_URL=https://base-rpc.example
# Required by commands whose active targetRate strategy is variable_rate_avg.
REFERENCE_RPC_URL=https://base-archive-rpc.example
# Exactly one write-mode signer source is required. The method is derived for legacy
# MAKER_PRIVATE_KEY-only deployments, but setting it explicitly is recommended.
Expand All @@ -19,6 +20,7 @@ MIDNIGHT_ADDRESS=0x2222222222222222222222222222222222222222
LOAN_ASSET_ADDRESS=0x3333333333333333333333333333333333333333
RATIFIER_ADDRESS=0x4444444444444444444444444444444444444444
MARKET_IDS=0x5555555555555555555555555555555555555555555555555555555555555555
# Required by commands whose active targetRate strategy is variable_rate_avg.
REFERENCE_MARKET_ID=0x7777777777777777777777777777777777777777777777777777777777777777
NATIVE_RESERVE_WEI=10000000000000000
MAXIMUM_LEND_EXPOSURE_ASSETS=10000000000
Expand All @@ -34,6 +36,6 @@ BETTERSTACK_INGESTING_HOST=
# Optional Better Stack heartbeat URL. Heartbeat failures never interrupt market making.
BETTERSTACK_HEARTBEAT_URL=
# BOOTSTRAP_MARKETS replaces the YAML list. Every integer property must be a quoted decimal string.
BOOTSTRAP_MARKETS=[{"marketId":"0x5555555555555555555555555555555555555555555555555555555555555555","creditTarget":"10000000000","acceptanceAssets":"100000000","offerSize":"500000000","premiumBps":"-50","maximumMarketExposure":"20000000000","maximumTotalExposure":"30000000000","minimumRateBps":"200","maximumRateBps":"800","autoRefill":false}]
BOOTSTRAP_MARKETS=[{"marketId":"0x5555555555555555555555555555555555555555555555555555555555555555","targetRate":{"strategy":"variable_rate_avg"},"creditTarget":"10000000000","acceptanceAssets":"100000000","offerSize":"500000000","premiumBps":"-50","maximumMarketExposure":"20000000000","maximumTotalExposure":"30000000000","minimumRateBps":"200","maximumRateBps":"800","autoRefill":false}]
# LADDER_MARKETS replaces YAML ladder. Every integer-valued property must be a quoted decimal string.
LADDER_MARKETS=[{"marketId":"0x5555555555555555555555555555555555555555555555555555555555555555","quotePremiumBps":"0","spreadBps":"200","stepBps":"100","rungCount":"3","sizeSkewBps":"0","lowerRateBudgetAssets":"10000000000","higherRateBudgetAssets":"10000000000","targetMarketExposureAssets":"20000000000","maximumTotalExposureAssets":"30000000000","minimumOfferAssets":"101000000","groupMode":"shared-rung","loopIntervalSeconds":"60","movementToleranceBps":"10","minimumRateBps":"200","maximumRateBps":"800"}]
LADDER_MARKETS=[{"marketId":"0x5555555555555555555555555555555555555555555555555555555555555555","targetRate":{"strategy":"hardcoded","hardcodedRateBps":"400"},"quotePremiumBps":"0","spreadBps":"200","stepBps":"100","rungCount":"3","sizeSkewBps":"0","lowerRateBudgetAssets":"10000000000","higherRateBudgetAssets":"10000000000","targetMarketExposureAssets":"20000000000","maximumTotalExposureAssets":"30000000000","minimumOfferAssets":"101000000","groupMode":"shared-rung","loopIntervalSeconds":"60","movementToleranceBps":"10","minimumRateBps":"200","maximumRateBps":"800"}]
92 changes: 61 additions & 31 deletions bots/market-making/README.md

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions bots/market-making/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ services:
environment:
CHAIN_ID: ${CHAIN_ID:-8453}
RPC_URL: ${RPC_URL:?set RPC_URL}
REFERENCE_RPC_URL: ${REFERENCE_RPC_URL:?set REFERENCE_RPC_URL}
REFERENCE_RPC_URL: ${REFERENCE_RPC_URL:-}
MAKER_PRIVATE_KEY: ${MAKER_PRIVATE_KEY:?set MAKER_PRIVATE_KEY}
MAKER_ADDRESS: ${MAKER_ADDRESS:?set MAKER_ADDRESS}
MIDNIGHT_ADDRESS: ${MIDNIGHT_ADDRESS:?set MIDNIGHT_ADDRESS}
Expand All @@ -16,7 +16,7 @@ services:
MORPHO_API_BASE_URL: ${MORPHO_API_BASE_URL:?set MORPHO_API_BASE_URL}
ROUTER_API_BASE_URL: ${ROUTER_API_BASE_URL:?set ROUTER_API_BASE_URL}
MARKET_IDS: ${MARKET_IDS:?set MARKET_IDS}
REFERENCE_MARKET_ID: ${REFERENCE_MARKET_ID:?set REFERENCE_MARKET_ID}
REFERENCE_MARKET_ID: ${REFERENCE_MARKET_ID:-}
V0_OFFER_GROUP_IDS: ${V0_OFFER_GROUP_IDS:-}
NATIVE_RESERVE_WEI: ${NATIVE_RESERVE_WEI:?set NATIVE_RESERVE_WEI}
MAXIMUM_LEND_EXPOSURE_ASSETS: ${MAXIMUM_LEND_EXPOSURE_ASSETS:?set MAXIMUM_LEND_EXPOSURE_ASSETS}
Expand Down
9 changes: 9 additions & 0 deletions bots/market-making/market-making.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
chain:
id: 8453
rpcUrl: 'https://base-rpc.example'
# Required by commands whose active targetRate strategy is variable_rate_avg.
archiveRpcUrl: 'https://base-archive-rpc.example'

identity:
Expand Down Expand Up @@ -32,6 +33,7 @@ markets:
allowlist:
- '0x5555555555555555555555555555555555555555555555555555555555555555'
- '0x6666666666666666666666666666666666666666666666666666666666666666'
# Required by commands whose active targetRate strategy is variable_rate_avg.
referenceMarketId: '0x7777777777777777777777777777777777777777777777777777777777777777'
v0OfferGroupIds:
- '0x8888888888888888888888888888888888888888888888888888888888888888'
Expand All @@ -46,6 +48,9 @@ setup:
# Asset/credit/exposure amounts are exact raw loan-asset units. Rates are integer BPS.
bootstrap:
- marketId: '0x5555555555555555555555555555555555555555555555555555555555555555'
targetRate:
strategy: 'hardcoded'
hardcodedRateBps: '400'
creditTarget: '10000000000'
acceptanceAssets: '100000000'
offerSize: '500000000'
Expand All @@ -56,6 +61,8 @@ bootstrap:
maximumRateBps: 800
autoRefill: false
- marketId: '0x6666666666666666666666666666666666666666666666666666666666666666'
targetRate:
strategy: 'variable_rate_avg'
creditTarget: '5000000000'
acceptanceAssets: '50000000'
offerSize: '250000000'
Expand All @@ -72,6 +79,8 @@ bootstrap:
# 10000+k*sizeSkewBps; the outermost funded rung receives division remainder.
ladder:
- marketId: '0x5555555555555555555555555555555555555555555555555555555555555555'
targetRate:
strategy: 'variable_rate_avg'
quotePremiumBps: '0'
spreadBps: '200'
stepBps: '100'
Expand Down
48 changes: 43 additions & 5 deletions bots/market-making/playground/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
import React, { Component, useEffect, useRef, useState } from 'react'
import { createRoot } from 'react-dom/client'

import type { FieldDefinition } from './field-visibility.utils'
import type {
BootstrapGraphicModel,
BootstrapInput,
Expand All @@ -19,6 +20,7 @@ import type {
} from './model'

import { CollectionImportError } from './collection-import.error'
import { visibleFields } from './field-visibility.utils'
import {
BOOTSTRAP_FIELDS,
LADDER_FIELDS,
Expand All @@ -43,7 +45,6 @@ import { playgroundErrorMessage } from './playground-error.utils'
import { PlaygroundInitializationError } from './playground-initialization.error'

type CollectionKind = keyof PlaygroundState
type FieldDefinition = readonly [string, string, string, string]
type ExportFormat = 'bootstrap-json' | 'bootstrap-string' | 'ladder-json' | 'ladder-string'
type Status = { message: string; status?: 'ok' | 'error' }
const EXPORT_FORMATS: ExportFormat[] = [
Expand Down Expand Up @@ -480,7 +481,7 @@ const Playground = () => {
</button>
</div>
<div className="field-grid">
{fields.map(([key, label, help, type]) => (
{visibleFields(fields, item.targetRate).map(([key, label, help, type]) => (
<form.Field
key={`${uiIds[kind][index]}-${key}`}
name={`${kind}.${index}.${key}` as never}
Expand All @@ -491,7 +492,30 @@ const Playground = () => {
<small>
{key} · {help}
</small>
{type === 'select' ? (
{type === 'target-rate-select' ? (
<select
id={`${kind}-${index}-${key}`}
value={item.targetRate.strategy}
onBlur={field.handleBlur}
onChange={event =>
form.setFieldValue(
`${kind}.${index}.targetRate` as never,
(event.target.value === 'hardcoded'
? {
strategy: 'hardcoded',
hardcodedRateBps:
item.targetRate.strategy === 'hardcoded'
? item.targetRate.hardcodedRateBps
: '500'
}
: { strategy: 'variable_rate_avg' }) as never
)
}
>
<option value="variable_rate_avg">variable_rate_avg</option>
<option value="hardcoded">hardcoded</option>
</select>
) : type === 'select' ? (
<select
id={`${kind}-${index}-${key}`}
value={String(field.state.value)}
Expand All @@ -505,9 +529,23 @@ const Playground = () => {
<input
id={`${kind}-${index}-${key}`}
type={type === 'checkbox' ? 'checkbox' : 'text'}
inputMode={type === 'number' ? 'numeric' : undefined}
inputMode={
type === 'number' || type === 'target-rate-number'
? 'numeric'
: undefined
}
checked={type === 'checkbox' ? Boolean(field.state.value) : undefined}
value={type === 'checkbox' ? undefined : String(field.state.value)}
value={
type === 'checkbox'
? undefined
: field.state.value === undefined
? ''
: String(field.state.value)
}
disabled={
type === 'target-rate-number' &&
item.targetRate.strategy !== 'hardcoded'
}
onBlur={field.handleBlur}
onChange={event =>
field.handleChange(
Expand Down
11 changes: 11 additions & 0 deletions bots/market-making/playground/field-visibility.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { TargetRateInput } from './model'

export type FieldDefinition = readonly [string, string, string, string]

export const visibleFields = (
fields: readonly FieldDefinition[],
targetRate: TargetRateInput
): readonly FieldDefinition[] =>
fields.filter(
([key]) => key !== 'targetRate.hardcodedRateBps' || targetRate.strategy === 'hardcoded'
)
66 changes: 52 additions & 14 deletions bots/market-making/playground/model.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { BootstrapConfig } from '../src/domain/bootstrap/position-bootstrap'
import type { LadderConfig } from '../src/domain/ladder/ladder'
import type { TargetRateConfigured } from '../src/domain/target-rate'

import {
BOOTSTRAP_MARKET_FIELDS,
Expand All @@ -15,11 +16,17 @@ import { FragmentCodecError } from './fragment-codec.error'
import { PreviewGenerationError } from './preview-generation.error'
import { StrictJsonError } from './strict-json.error'

export type TargetRateInput =
| { strategy: 'variable_rate_avg' }
| { strategy: 'hardcoded'; hardcodedRateBps: string }
export type BootstrapInput = Record<
Exclude<(typeof BOOTSTRAP_MARKET_FIELDS)[number], 'autoRefill'>,
Exclude<(typeof BOOTSTRAP_MARKET_FIELDS)[number], 'autoRefill' | 'targetRate'>,
string
> & { autoRefill: boolean }
export type LadderInput = Record<(typeof LADDER_MARKET_FIELDS)[number], string>
> & { autoRefill: boolean; targetRate: TargetRateInput }
export type LadderInput = Record<
Exclude<(typeof LADDER_MARKET_FIELDS)[number], 'targetRate'>,
string
> & { targetRate: TargetRateInput }

export type PlaygroundState = {
bootstrap: BootstrapInput[]
Expand All @@ -28,6 +35,13 @@ export type PlaygroundState = {

export const BOOTSTRAP_FIELDS = [
['marketId', 'Market ID', '0x-prefixed bytes32 market', 'text'],
['targetRate.strategy', 'Target rate', 'Reference-rate strategy', 'target-rate-select'],
[
'targetRate.hardcodedRateBps',
'Hardcoded target rate (BPS)',
'Positive reference rate used by the hardcoded strategy',
'target-rate-number'
],
['creditTarget', 'Credit target', 'Positive raw credit units', 'number'],
['acceptanceAssets', 'Completion threshold', 'Allowed target shortfall', 'number'],
['offerSize', 'Pending-offer cap', 'Maximum desired offer assets', 'number'],
Expand All @@ -40,6 +54,13 @@ export const BOOTSTRAP_FIELDS = [
] as const
export const LADDER_FIELDS = [
['marketId', 'Market ID', '0x-prefixed bytes32 market', 'text'],
['targetRate.strategy', 'Target rate', 'Reference-rate strategy', 'target-rate-select'],
[
'targetRate.hardcodedRateBps',
'Hardcoded target rate (BPS)',
'Positive reference rate used by the hardcoded strategy',
'target-rate-number'
],
['quotePremiumBps', 'Quote premium (BPS)', 'Signed center offset', 'number'],
['spreadBps', 'Full spread (BPS)', 'Positive even nearest-rung distance', 'number'],
['stepBps', 'Step (BPS)', 'Positive same-side rung distance', 'number'],
Expand All @@ -61,6 +82,7 @@ const DEFAULT_MARKET_ID = `0x${'5'.repeat(64)}`

export const createDefaultBootstrap = (marketId = DEFAULT_MARKET_ID): BootstrapInput => ({
marketId,
targetRate: { strategy: 'variable_rate_avg' },
creditTarget: '10000000000',
acceptanceAssets: '100000000',
offerSize: '500000000',
Expand All @@ -74,6 +96,7 @@ export const createDefaultBootstrap = (marketId = DEFAULT_MARKET_ID): BootstrapI

export const createDefaultLadder = (marketId = DEFAULT_MARKET_ID): LadderInput => ({
marketId,
targetRate: { strategy: 'variable_rate_avg' },
quotePremiumBps: '0',
spreadBps: '200',
stepBps: '100',
Expand All @@ -96,8 +119,14 @@ export const createDefaultPlaygroundState = (): PlaygroundState => ({
ladder: [createDefaultLadder()]
})

const bootstrapInput = (config: BootstrapConfig): BootstrapInput => ({
const targetRateInput = (config: TargetRateConfigured<unknown>['targetRate']): TargetRateInput =>
config.strategy === 'hardcoded'
? { strategy: 'hardcoded', hardcodedRateBps: String(config.hardcodedRateBps) }
Comment thread
prd-carapulse[bot] marked this conversation as resolved.
: { strategy: 'variable_rate_avg' }

const bootstrapInput = (config: TargetRateConfigured<BootstrapConfig>): BootstrapInput => ({
marketId: config.marketId,
targetRate: targetRateInput(config.targetRate),
creditTarget: String(config.creditTarget),
acceptanceAssets: String(config.acceptanceAssets),
offerSize: String(config.offerSize),
Expand All @@ -109,8 +138,9 @@ const bootstrapInput = (config: BootstrapConfig): BootstrapInput => ({
autoRefill: config.autoRefill
})

const ladderInput = (config: LadderConfig): LadderInput => ({
const ladderInput = (config: TargetRateConfigured<LadderConfig>): LadderInput => ({
marketId: config.marketId,
targetRate: targetRateInput(config.targetRate),
quotePremiumBps: String(config.quotePremiumBps),
spreadBps: String(config.spreadBps),
stepBps: String(config.stepBps),
Expand Down Expand Up @@ -173,14 +203,17 @@ export const deriveBootstrapGraphicModels = (items: BootstrapInput[]): Bootstrap
parseBootstrap(items).map(item => {
const minimum = BigInt(item.minimumRateBps)
const maximum = BigInt(item.maximumRateBps)
const quoted = (minimum + maximum) / 2n
const reference = quoted - BigInt(item.premiumBps)
const premium = BigInt(item.premiumBps)
const reference =
item.targetRate.strategy === 'hardcoded'
? BigInt(item.targetRate.hardcodedRateBps)
: (minimum + maximum) / 2n - premium
Comment thread
prd-carapulse[bot] marked this conversation as resolved.
const quoted = reference + premium
if (
reference <= 0n ||
reference < minimum ||
reference > maximum ||
reference + BigInt(item.premiumBps) < minimum ||
reference + BigInt(item.premiumBps) > maximum
(item.targetRate.strategy !== 'hardcoded' && (reference < minimum || reference > maximum)) ||
quoted < minimum ||
quoted > maximum
) {
throw new PreviewGenerationError(
'Bootstrap derived reference and quoted rates must be positive and remain inside configured bounds'
Expand Down Expand Up @@ -266,9 +299,14 @@ export const generateLadderGraphicModels = (
)[0]!
const minimum = config.minimumRateBps
const maximum = config.maximumRateBps
const center = (minimum + maximum) / 2n
const reference = center - config.quotePremiumBps
if (reference <= 0n || reference < minimum || reference > maximum) {
const reference =
input.targetRate.strategy === 'hardcoded'
? BigInt(input.targetRate.hardcodedRateBps)
: (minimum + maximum) / 2n - config.quotePremiumBps
if (
reference <= 0n ||
(input.targetRate.strategy !== 'hardcoded' && (reference < minimum || reference > maximum))
) {
throw new PreviewGenerationError(
'Ladder derived reference and center rates must remain inside configured bounds'
)
Expand Down
10 changes: 5 additions & 5 deletions bots/market-making/scripts/deploy-railway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { resolve } from 'node:path'

import { RailwayDeploymentError } from './railway-deployment.error'
import {
assertFreshRailwayReferenceProvisioning,
assertFullRailwaySignerProvisioning,
isNonEmptyJsonArray,
isTerminalRailwayDeploymentStatus,
Expand All @@ -36,15 +37,13 @@ if (!PROJECT_ID) {
const requiredRuntimeVariableNames = [
'CHAIN_ID',
'RPC_URL',
'REFERENCE_RPC_URL',
'MAKER_ADDRESS',
Comment thread
prd-carapulse[bot] marked this conversation as resolved.
'MIDNIGHT_ADDRESS',
'LOAN_ASSET_ADDRESS',
'RATIFIER_ADDRESS',
'MORPHO_API_BASE_URL',
'ROUTER_API_BASE_URL',
'MARKET_IDS',
'REFERENCE_MARKET_ID',
'NATIVE_RESERVE_WEI',
'MAXIMUM_LEND_EXPOSURE_ASSETS',
'BOOTSTRAP_MARKETS',
Expand Down Expand Up @@ -132,8 +131,9 @@ const listServices = async () => {
const ensureService = async () => {
const services = await listServices()
const existingService = services.find(service => service.name === SERVICE)
if (existingService) return existingService
if (existingService) return { service: existingService, isFreshService: false }

assertFreshRailwayReferenceProvisioning(Bun.env, true)
const { data, error } = await tryCatch(
Promise.resolve($`railway add --service ${SERVICE} --json`.quiet().text())
)
Expand All @@ -146,7 +146,7 @@ const ensureService = async () => {
throw new RailwayDeploymentError('Railway service creation returned incomplete identity')
}

return createdService
return { service: createdService, isFreshService: true }
}

const listVolumes = async () => {
Expand Down Expand Up @@ -272,7 +272,7 @@ await assertCli()
await ensureContext()

if (!DEPLOY_ONLY) {
const service = await ensureService()
const { service } = await ensureService()

await setRuntimeVariable(['RAILWAY_DOCKERFILE_PATH', DOCKERFILE_PATH])
await setRuntimeVariable(['XDG_STATE_HOME', STATE_MOUNT_PATH])
Expand Down
Loading
Loading