@steerprotocol/clamm-vm
v0.1.0
Published
Deterministic Uniswap v3 pool simulation for TypeScript.
Downloads
82
Readme
clamm-vm
In-memory Uniswap v3 pool simulation for TypeScript.
Package entry points
// Deterministic pool, token, tick, position, and swap simulation.
import { FeeAmount, Pool, createToken } from "@steerprotocol/clamm-vm";
// Synthetic backtests, actors, reports, market data, and visualizations.
import { createSyntheticBacktest, createStrategyActor } from "@steerprotocol/clamm-vm/simulation";
// Panoptic accounting, risk, and protocol simulation.
import { PanopticPoolVM } from "@steerprotocol/clamm-vm/panoptic";The Pool object starts empty at a configured price. Users add and remove liquidity through pool methods, then swaps mutate price, current tick, active liquidity, tick fee-growth state, and LP fee accounting.
import { FeeAmount, Pool, createToken } from "@steerprotocol/clamm-vm";
const usdc = createToken(1, "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", 6, "USDC");
const weth = createToken(1, "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", 18, "WETH");
const pool = Pool.fromTick({
tokenA: usdc,
tokenB: weth,
fee: FeeAmount.MEDIUM,
tickCurrent: 0
});
const added = pool.addLiquidity({
owner: "alice",
tickLower: -600,
tickUpper: 600,
liquidity: 1_000_000n
});
const trade = await pool.swapExactIn({ tokenIn: pool.token0, amountIn: 1_000n });
const fees = pool.poke({ owner: "alice", tickLower: -600, tickUpper: 600 });
const collected = pool.collect({ owner: "alice", tickLower: -600, tickUpper: 600 });
console.log({
required0: added.amount0.toString(),
required1: added.amount1.toString(),
amountOut: trade.amountOut.toString(),
fees0: fees.tokensOwed0.toString(),
collected0: collected.amount0.toString()
});Scope
This package targets Uniswap v3-style deterministic pool behavior:
- swap price/tick traversal
- active liquidity changes while crossing ticks
- LP fee growth globals
- tick fee-growth-outside accounting
- per-position fee snapshots and owed token accounting
- burn and collect flows
It does not model block-time oracle observations, TWAP accumulators, seconds-per-liquidity, callbacks, token balances, or Uniswap v4 hooks/singleton behavior.
Synthetic Market GIFs
For longer synthetic runs, use the horizon builder. You describe calendar time,
block time, liquidity health, activity, and candle interval; the simulator handles
block timestamps, stacked intra-block trades, block summaries, candles, and GIF
rendering. The liquidity-health model in the horizon builder creates a seeded
ecology of persistent LP actors; health biases their persona mix, capital,
range widths, placement drift, stale behavior, and rebalancing cadence instead of
selecting one fixed liquidity chart shape.
import { createSyntheticBacktest, renderMarketGif } from "@steerprotocol/clamm-vm/simulation";
const backtest = createSyntheticBacktest({
pool,
seed: "two-month-demo",
horizon: {
start: "2026-01-01T00:00:00Z",
duration: "60d",
blockTime: "12s"
},
liquidity: {
model: "liquidity-health",
owner: "market-lps",
health: 7,
totalLiquidity: "7800000"
},
activity: {
profile: "normal",
avgTradesPerDay: 1200,
avgVolumeToken1PerDay: "5000000"
},
candles: { interval: "1d" }
});
const result = await backtest.run();
const gif = renderMarketGif({
blocks: result.blocks,
candles: result.candles,
priceView: "candles",
width: 900,
height: 540
});Synthetic backtests can also run first-class single-pool strategies. Strategies return balance-enforced intents, so invalid swaps or LP changes are rejected by the strategy actor before they become market events. Reports use token1 as the default quote currency for NAV, PnL, drawdown, HODL comparison, and estimated IL.
import { analyzeBacktest, createStrategyActor, createSyntheticBacktest } from "@steerprotocol/clamm-vm/simulation";
const strategy = createStrategyActor({
id: "range-lp",
initialBalances: { token0: "5000000", token1: "5000000" },
strategy: {
initialize(ctx) {
return [{
type: "addLiquidity",
tickLower: ctx.pool.tickCurrent - 600,
tickUpper: ctx.pool.tickCurrent + 600,
liquidity: "2500000"
}];
},
onBlock() {
return [];
}
}
});
const result = await createSyntheticBacktest({
pool,
seed: "strategy-demo",
horizon: { start: "2026-01-01T00:00:00Z", duration: "14d", blockTime: "1m" },
regimes: { initialRegime: "normal" },
liquidity: { model: "liquidity-health", owner: "market-lps", health: 7, totalLiquidity: "7800000" },
activity: { profile: "normal" },
actors: [strategy],
candles: { interval: "4h" },
outputs: { recordActorSnapshots: true }
}).run();
const report = analyzeBacktest(result);For repeatable Panoptic strategy runs, use the strategy runner. Strategy modules
live in strategies/; the runner owns explicit market-source selection, market
setup, report writing, and GIF rendering. Synthetic runs retain the existing
liquidity ecology. Portable profiles calibrate that ecology without adding a
per-run network dependency; optional one-block snapshots seed exact aggregate
Pool depth. Historical runs replace the ecology, price process, flow, and fixed
clock with one verified local tape or retained benchmark.
See Market Data And Strategy Operations for the complete import, observation, profile, snapshot, benchmark, generation, stress, strategy, cost-governance, comparison, and review workflow.
When no tape or calibration is selected, the runner keeps the existing
synthetic actor ecology for compatibility and labels it exploratory. Its
execution mechanics are configured independently: --synthetic-arbitrage
selects bounded-actor, price-target, or disabled, while
--organic-external-gap-bias-multiplier,
--organic-external-return-bias-multiplier, and --organic-max-side-bias
control synthetic flow response. These settings are recorded as
marketExecution; they do not imply empirical marketFidelity.
npm run strategy -- listRun the two included Panoptic strategy modules:
# Full gamma-scalp run with reports and panoptic.gif
npm run strategy -- panoptic-gamma-scalp
# Short-call smoke run with reports and panoptic.gif
npm run strategy -- panoptic-short-call --duration 1d --block-time 4hRun any strategy module against an immutable local tape without RPC or subgraph access during execution:
npm run strategy -- path/to/strategy.mjs \
--market-tape market-data/ethereum-usdc-weth-500/20000000-20010000Create one deterministic native contiguous window from a larger tape and its source calibration:
npm run market-data:bootstrap -- \
--tape market-data/pool/training \
--calibration market-data/calibrations/pool-v1 \
--output market-data/bootstrap/pool-window-1 \
--seed pool-window-1 \
--blocks 50400The sampled tape derives its complete parent Pool state and custody by exact prefix replay. It preserves source block numbers, hashes, timestamps, prices, ticks, liquidity, action order, and baseline event volume. The schema records one source window and an empty splice-boundary list; multi-window splicing is rejected rather than silently rebasing state.
Generate a hypothetical market from an empirically ready calibration. The
target price and liquidity are explicit; fee tier and token identities inherit
the source pool unless --pool supplies another pool identity:
npm run market-data:generate -- \
--calibration market-data/calibrations/pool-v1 \
--output market-data/generated/pool-seed-1 \
--seed pool-seed-1 \
--blocks 50400 \
--segment-length 256 \
--initial-price 0.0005 \
--liquidity 1000000000000000000 \
--parent-block 20010000 \
--parent-hash 0x0000000000000000000000000000000000000000000000000000000000000000 \
--parent-timestamp 2026-01-01T00:00:00.000ZThe command writes an ordinary immutable tape plus market-frames.jsonl, one
arbitrage decision per frame, envelope rows, and generation fidelity reports.
The Pool still moves only through normal swaps and LP actions. A shared input
envelope prevents organic and arbitrage-like flow from independently creating
baseline volume.
Apply an ordered correlated stress composition to the same calibrated frame source before Pool execution:
npm run market-data:stress -- \
--calibration market-data/calibrations/pool-v1 \
--stress-config examples/market-stress/liquidity-crunch.json \
--output market-data/stressed/liquidity-crunch-seed-1 \
--seed pool-seed-1 \
--blocks 50400 \
--segment-length 256 \
--initial-price 0.0005 \
--liquidity 1000000000000000000 \
--parent-block 20010000 \
--parent-hash 0x0000000000000000000000000000000000000000000000000000000000000000 \
--parent-timestamp 2026-01-01T00:00:00.000ZThe JSON file is required. It owns caller-labeled severity and an ordered list
of inclusive frame windows. Supported operators are return-shock,
volume-response, directional-imbalance, liquidity-withdrawal,
lp-latency, and arbitrage-capacity. The complete transformed frame stream is
preflighted before a Pool is created. Invalid volume, liquidity, delayed LP
actions beyond the horizon, or arbitrage capacity beyond the shared envelope
fail before execution. The command writes an immutable scripted-stress tape,
exact envelope rows, and a distinct stress-fidelity report. It never assigns
Pool price, tick, fee growth, or live liquidity directly.
Run a strategy directly from calibration by supplying the same generation inputs. The generated tape is materialized under the result folder and the strategy then uses the existing tape runner:
npm run strategy -- path/to/strategy.mjs \
--market-calibration market-data/calibrations/pool-v1 \
--market-generation-blocks 50400 \
--market-generation-segment-length 256 \
--market-generation-initial-price 0.0005 \
--market-generation-liquidity 1000000000000000000 \
--market-generation-seed strategy-market-1 \
--output-mode metricsAdd one stress composition to that strategy run with --market-stress:
npm run strategy -- path/to/strategy.mjs \
--market-calibration market-data/calibrations/pool-v1 \
--market-generation-blocks 50400 \
--market-generation-segment-length 256 \
--market-generation-initial-price 0.0005 \
--market-generation-liquidity 1000000000000000000 \
--market-generation-seed strategy-market-1 \
--market-stress examples/market-stress/liquidity-crunch.json \
--output-mode metricsArbitrage capacity defaults to zero. Enabling it requires explicit finite raw
token capital and supports explicit operational cost, reference latency, and
cooldown inputs. Generated execution fidelity has no hidden tolerance defaults:
all five --market-generation-fidelity-* limits must be supplied together
before the market can be labeled decision-grade. Without them, transformation,
source-sample, envelope, and exact-replay evidence still runs, while execution
fidelity remains unassessed.
The CLI market reference overrides an optional strategy default. Strategy defaults resolve relative to the strategy module:
export default {
market: {
tape: "../market-data/pool/training",
window: {
calibration: "../market-data/calibrations/pool-v1",
blocks: 50400,
seed: "default-window"
}
},
create(context) {
// context includes tokens, initialPoolSnapshot, poolIdentity, and marketSource.
}
};A tape owns its block range, variable timestamps, initial Pool state and
custody, liquidity, baseline flow, and price path. Synthetic-only clock,
liquidity, volatility, activity, and arbitrage flags are rejected rather than
silently ignored. Counterfactual limits remain caller supplied through
--max-strategy-volume-bps, --max-strategy-active-liquidity-bps,
--max-panoptic-active-liquidity-bps, --max-panoptic-maker-depth-bps, and
--max-pool-tick-divergence.
--market-calibration requires a complete market.generation config or matching
--market-generation-* inputs; a bare calibration remains descriptive and
fails explicitly. --market-window-calibration instead pairs a calibration
with its exact source tape for native contiguous-window sampling.
Strategy modules can return a liquidity object to choose their own liquidity
ecology profile. CLI flags such as --liquidity-health and
--total-liquidity override that profile for one-off runs.
The gamma-scalp actor hedges the strategy portfolio's decision-grade, closeout-equivalent token0 exposure relative to its passive initial holdings. That exposure includes live wallet cash, Panoptic collateral, and the VM's rollback-safe immediate-close token vector. Normal hedges use a configurable tracking band and trade-size cap; after burning the option, the actor continues until the stricter terminal exposure tolerance is satisfied. It does not use an approximate Black-Scholes or sigmoid delta model.
Strategy swaps and Panoptic deposits use the same explicit finite wallets. A strategy module declares wallets and collateral deposits independently of its liquidity ecology:
return {
funding: {
wallets: {
"my-strategy-actor": { token0: "50000000", token1: "51000000" },
"market-maker": { token0: "100000000", token1: "100000000" }
},
panopticCollateral: [
{ wallet: "my-strategy-actor", account: "alice", token0: "50000000", token1: "50000000" },
{ wallet: "market-maker", account: "market-maker", token0: "100000000", token1: "100000000" }
]
}
};The runner separately funds liquidity ecology, organic flow, and arbitrage from
recorded assumptions. Deposits debit the declared wallet into one collateral
tracker per underlying token. Direct Uniswap actions and Panoptic settlement use
the same capital ledger, so insufficient actions roll back and physical custody
reconciles across wallets, collateral, and the shared Pool. The real
PanopticCollateralSettlement uses live solvency, the Panoptic-owned oracle
maintains historical risk ticks, and collateral interest accrues from the shared
block clock. Dispatches update the oracle through their normal VM path; an
optional --panoptic-oracle-keeper-steps actor can call pokeOracle explicitly.
The simulation never mutates Panoptic observations automatically once per block.
Live blocks use an explicit default transaction order: market observation,
liquidity maintenance, organic flow, pre-strategy arbitrage, strategy actions,
then optional post-strategy arbitrage. A strategy module can return
actorExecution and transactionPhaseOrder to place its actors or reorder those
phases explicitly. Genesis actions use the preceding source block number, and the
resolved clock plus transaction order are written to manifest.json. Within a
phase, explicit priority wins and actor id is the stable tie-breaker; actor array
position does not change execution order.
Each run writes a timestamped folder under backtest-results/<strategy-id>/
with:
manifest.jsonrun-receipt.jsonvalidation.jsonsummary.jsonfinal-panoptic-report.jsonfinal-strategy-portfolio.jsoncapital.jsonpanoptic-market-depth.jsonmarket-source.jsonmarket-fidelity.jsonresearch-summary.jsonmarket-diagnostics.jsonmarket-actions.jsonlmarket-metrics.csvportfolio-metrics.csvhedge-trades.csvcandles.csvsources/*.jsonlpanoptic.gif
Historical runs additionally write market-replay.json with exact source
action/checkpoint reconciliation, baseline-volume conservation, endogenous
participation, Pool divergence, and counterfactual-limit status.
Generated runs also write generated-market-fidelity.json; their generated tape
and source-owned generation reports live under generated-market/ for a single
run or generated-markets/ for a matrix run.
Stressed runs instead write stressed-market-fidelity.json; their tape and
source-owned stress reports live under stressed-market/ for one run or
stressed-markets/ for a matrix. Generated and stress readiness classes are not
combined.
Debug mode additionally writes the retained event, block, candle, analysis, and
full report files. Debug, sampled, and metrics modes use the same source-owned
economic calculations, compact rows, source ledgers, final reports, validation,
and GIF renderer. They differ only in retained event, block, actor, and debug
report density. market-actions.jsonl is the compact outcome stream for every
market action; successful Panoptic rows carry their receipt actionSequence and
actionId.
The sources/ directory persists action receipts, contract events, accounting,
premium, complete tracker-owned collateral event logs, collateral/risk/account/
closeout reports, strategy reports, and the liquidation source report. The
validator checks source sequences and joins, capital and collateral custody,
closeout and strategy NAV arithmetic, final compact-row consistency, and
decision-grade readiness. A structurally valid run with unavailable sources is
labeled non-decision-grade instead of silently publishing NAV.
manifest.json records the implementation and Panoptic reference commits,
working-tree content hash including non-ignored untracked files, installed
Uniswap package versions, capabilities, seeds, tokens, initial Pool state,
liquidity, capital, clock, transaction order, execution assumptions, and resolved
strategy parameters. It labels the configured GBM input as external volatility
and records market-fidelity readiness independently from accounting readiness.
run-receipt.json records timing, event/action/source row
counts, failures, economics, artifact paths, and validation status.
capital.json contains initial and final wallets, Uniswap and Panoptic custody,
attributed settlement rows, capacity rejections, and an exact token-conservation
check. Historical baseline actions settle through an explicit signed external
source boundary: Pool custody changes by the executed VM result without
fabricating finite wallets for unknown historical participants. Strategy wallets
and Panoptic collateral remain finite. panoptic-market-depth.json keeps external
AMM liquidity, SFPM option liquidity, collateral depth/utilization, and remaining
actor capital separate.
research-summary.json records result-validation status, closeout NAV, exact
economic PnL, the passive-hold benchmark bridge, turnover, drawdown, exposure,
execution costs, and actor-level failure counts. Economic fields fail closed unless
both canonical portfolio readiness and result validation are decision-grade.
Drawdown and exposure use the complete compact per-block portfolio series in every
output mode, independent of retained debug snapshots.
market-diagnostics.json is computed from the complete source-owned block stream.
It reports configured external volatility separately from measured external and
Pool realized volatility, volatility transmission, return beta/correlation,
tracking error, Pool/reference gap percentiles, active-liquidity percentiles,
base-versus-Panoptic active-liquidity ownership, and swap volume split across
organic, arbitrage-like, strategy, other, and total flow. Synthetic runs remain
explicitly labeled exploratory. Historical runs report exact replay or
counterfactual readiness for the selected window independently from accounting
readiness; exact replay does not claim that one window is broadly representative.
Contiguous-bootstrap runs add market-bootstrap-fidelity.json. They are
decision-grade for market fidelity only when the sampled window passes the
source calibration's explicit criteria; missing criteria and insufficient
samples remain unassessed or sample-limited. A single sampled window always
retains representative: false.
Producing a calibration artifact alone does not change the source used by a run.
Market terminology is deliberately attribution-based. For synthetic runs,
baseline is organic plus arbitrage-like flow before strategy additions. For
historical runs, baseline is ordered source-tape flow; unknown source traders
are not classified as organic or arbitrage. organic means swaps
executed in the organic-flow phase. arbitrage-like means swaps executed in a
pre- or post-strategy arbitrage phase; it is a phase attribution, not a claim
about an unknown trader's economics. strategy means swaps executed in the
strategy phase, while hedge is the intent-labeled subset of strategy flow.
Panoptic-owned active liquidity means active Pool positions using the canonical
SFPM owner prefix; every other active position remains base-market liquidity.
Canonical historical market tapes support legacy uniswap-v3-market-tape/v1
artifacts and current uniswap-v3-market-tape/v2 imports. V2 uses a required
hashed uniswap-v3-initial-pool-state/v4 parent snapshot, canonical
blocks.jsonl, and optional checkpoints.jsonl. openMarketTape() parses the manifest first,
streams every referenced file to verify schema, block ancestry, row counts, byte
lengths, and SHA-256 hashes, and only then returns bounded-memory
block/checkpoint iterators. The verified artifact is deep-frozen.
writeMarketTape() validates while streaming and writes the manifest last, so a
partial write cannot look ready. Local root-level market-data/ artifacts are
ignored; small frozen fixtures live under test/fixtures/market-tapes/.
Build a local tape from a UTC range with subgraph-indexed events and an exact archive-RPC parent snapshot:
ETH_RPC_URL=https://... \
UNISWAP_V3_SUBGRAPH_URL=https://... npm run market-data:import -- \
--chain-id 1 \
--pool 0x88e6a0c2ddd26feeb64f039a2c41296fcb3f5640 \
--start-time 2026-07-01T00:00:00.000Z \
--end-time 2026-07-08T00:00:00.000Z \
--subgraph-url-env UNISWAP_V3_SUBGRAPH_URL \
--market-id ethereum-usdc-weth-500 \
--output market-data/ethereum-usdc-weth-500/2026-07-01_2026-07-08 \
--checkpoint-cadence-blocks 300RPC credentials remain build-time inputs and are not persisted. The importer pins finality and block hashes, reads the parent-block tick bitmap and exact tick depth directly, hydrates only position keys referenced during the selected forward window, proves executable swap flow from source outcomes, verifies pages again before atomic publication, and emits source-owned checkpoints. It does not scan Mint history from Pool creation or instantiate historical LP participants. Contract reads use bounded Multicall3 chunks with individual fallback, while swap transaction calldata is fetched only when outcome reconciliation needs it. Checkpoint cadence and block hashes are enforced at both write and open boundaries. Normal strategy execution then uses only the local artifact.
The committed
test/fixtures/market-tapes/uniswap-v3-usdc-weth-first-swap/ fixture restores
the mainnet USDC/WETH 0.05% Pool at block 12376890 and reproduces the first swap
at block 12376891 exactly in raw integer amounts, sqrt ratio, tick, liquidity,
and checkpoint state.
Historical execution uses MarketTapeBaselineActor plus its injected
MarketTapeReplayCursor. The cursor drives the simulation with source block
numbers, hashes, timestamps, and elapsed time; Uniswap and Panoptic therefore
observe the same historical clock. Source actions run in strict transaction/log
order in the dedicated baseline-market phase. Strategy actions run afterward
in the explicit strategy phase and remain endogenous counterfactual flow.
MarketTapeBaselineActor.report() returns exact action/checkpoint reconciliation,
baseline-volume conservation, source mismatch rows, strategy volume, direct LP
and Panoptic active-liquidity participation, and Pool divergence. Counterfactual
readiness requires caller-supplied limits; the VM does not choose market-impact
limits on the strategy's behalf. Persist the report beside other outputs with
FileBacktestOutputSink.writeMarketTapeReplayReport().
Build a deterministic calibration from any verified tape that passes exact historical replay:
npm run market-data:calibrate -- \
--tape market-data/ethereum-usdc-weth-500/20000000-20010000 \
--output market-data/calibrations/ethereum-usdc-weth-500-trainingThe command writes canonical calibration.json and block-bundles.jsonl
files atomically. The artifact preserves source and pool identity, the parent
close used for the first return, raw-unit marginals, signed flow, portable
liquidity ratios, intraday rows, lag relationships, reference-gap response,
sample readiness, derivation kinds, and file hashes. openMarketCalibration()
streams and verifies both files and reconciles their source range, counts,
volumes, liquidity, returns, realized volatility, and readiness.
An artifact is not empirically-ready without explicit acceptance criteria and
a held-out tape for the same pool. A criteria file has this exact shape:
{
"minimumTrainingBlocks": 1000,
"minimumTrainingSwaps": 500,
"minimumTrainingMints": 10,
"minimumTrainingBurns": 10,
"minimumHeldOutBlocks": 500,
"minimumHeldOutSwaps": 250,
"minimumHeldOutMints": 5,
"minimumHeldOutBurns": 5,
"maximumDistributionMeanDifferenceBps": 1000,
"maximumDistributionP90DifferenceBps": 1500,
"maximumCorrelationAbsoluteDifference": 0.25
}npm run market-data:calibrate -- \
--tape market-data/pool/training \
--held-out-tape market-data/pool/held-out \
--criteria market-data/pool/criteria.json \
--output market-data/calibrations/pool-v1Independent local reference prices are optional and are never inferred from swap direction. Their file bytes are hashed into the artifact:
{
"sourceId": "independent-price-source",
"prices": [
{ "blockNumber": 20000000, "price1Per0": 0.00042 }
]
}Pass that file with --reference-prices; use
--held-out-reference-prices for the held-out window. Missing criteria leaves
the artifact source-complete, insufficient block/swap/mint/burn samples make
it sample-limited, and only a complete held-out diagnostic pass makes it
empirically-ready. Unsupported optional relationships remain labeled rather
than synthesized.
Imported execution reserves baseline ownership: synthetic liquidity, organic
flow, and pre/post-strategy arbitrage phases cannot be enabled implicitly. File
outputs classify imported swaps as baseline, not as organic or arbitrage-like,
because chain logs do not prove trader intent.
Swap logs and swap intent are intentionally separate. A Pool Swap event proves
token deltas and post-swap state, but not exact-input versus exact-output caller
intent. The tape only lowers a swap into a VM action when a source such as decoded
transaction calldata supplies that intent; otherwise it emits a partial row with
SwapIntentUnavailableFromPoolEvent instead of guessing.
The current decoder proves direct Pool calls and canonical Ethereum v3
SwapRouter single-pool calls. It also proves an exactInput multi-hop call when
the imported Pool is the first hop. Unknown routers, multicalls, and later route
hops remain partial. Nonzero protocol fees, unsupported Pool events, incomplete
parent state, and ranges that begin before an initialized parent state fail
closed instead of being approximated.
Use a matrix run to vary independent seeds, liquidity health, execution latency, strategy position size, and configured external-price-process volatility. Every matrix cell writes its own reports and GIF, while the parent folder receives exact raw-unit JSON and CSV summaries:
npm run strategy -- panoptic-gamma-scalp --seed-list seed-a,seed-b --liquidity-health-list 3,8 --strategy-latency-steps-list 0,1Keep market entropy separate from strategy behavior by crossing market-source and strategy seeds explicitly. For historical bootstrap sources:
npm run strategy -- path/to/strategy.mjs \
--market-tape market-data/pool/training \
--market-window-calibration market-data/calibrations/pool-v1 \
--market-window-blocks 50400 \
--market-window-seed-list market-a,market-b,market-c \
--seed-list strategy-a,strategy-b \
--output-mode metricsEach sampled tape is materialized once and reused across its strategy seeds. The parent JSON/CSV identifies the source tape, calibration, selected offset, and block range. It also reports duplicate-window concentration and pairwise source-block overlap; overlapping windows are labeled rather than counted as independent samples.
The same market-source seed dimension drives independent calibrated generated
markets. The market-window-* spelling is retained for matrix compatibility,
but generated outputs and summary dimensions are labeled generated-market:
npm run strategy -- path/to/strategy.mjs \
--market-calibration market-data/calibrations/pool-v1 \
--market-generation-blocks 50400 \
--market-generation-segment-length 256 \
--market-generation-initial-price 0.0005 \
--market-generation-liquidity 1000000000000000000 \
--market-window-seed-list generated-a,generated-b,generated-c \
--seed-list strategy-a,strategy-b \
--output-mode metricsCross explicit stress compositions as a separate matrix dimension. Every path is loaded and validated before execution; there is no implicit baseline or default stress cell:
npm run strategy -- path/to/strategy.mjs \
--market-calibration market-data/calibrations/pool-v1 \
--market-generation-blocks 50400 \
--market-generation-segment-length 256 \
--market-generation-initial-price 0.0005 \
--market-generation-liquidity 1000000000000000000 \
--market-stress-list examples/market-stress/liquidity-crunch.json,path/to/arb-outage.json \
--market-window-seed-list generated-a,generated-b \
--seed-list strategy-a,strategy-b \
--output-mode metricsThe parent JSON and CSV include stress id, caller-owned severity, config hash, and aggregation class per cell. Results group by exact stress config hash, so historical, bootstrap, generated, and stressed readiness cannot be merged by accident.
Keep maker inventory fixed while testing whether gamma-scalp economics change with option-depth consumption and volatility:
npm run strategy -- panoptic-gamma-scalp --seed-list size-vol-a,size-vol-b --position-size-list 100000,250000,500000,1000000 --maker-position-size 2000000 --external-volatility-list 0.25,0.65,1 --liquidity-health 9 --duration 7d --block-time 1h --output-mode metrics --frame-count 36The parent summary records excess value and explicit costs per million position
units, premium paid, maker-depth consumption, and hedge execution cost versus
both the pre-trade Pool price and the external reference. It also aggregates
market transmission, liquidity, flow attribution, and market-fidelity readiness.
The generic --volatility and --volatility-list spellings were removed after
consumer migration. The runner returns a precise replacement message because
these inputs configure only the synthetic external price process; use
--external-volatility and --external-volatility-list.
The examples runner includes a file-writing demo:
npm run example -- market-gifIt writes examples/output/market.gif. The quick default uses a shorter horizon,
but the same command supports a two-month daily-candle run:
npm run example -- market-gif --duration 60d --block-time 12s --candle 1d --activity normal --liquidity-health 7To compare every liquidity-health level with the same synthetic market tape, run:
npm run example -- market-gif-health-sweepThat writes examples/output/market-health-01.gif through
examples/output/market-health-10.gif. The sweep defaults are intentionally
short (3d, 5m blocks, 1h candles) so all ten GIFs render quickly; pass the
same flags as market-gif when you want a longer or denser diagnostic run.
Lower liquidity health scores bias the LP ecology toward sparse, thin, gappy,
skewed, and stale participation. Higher scores bias it toward deeper, more
continuous, more overlapping, and more actively maintained liquidity around the
traded price. Activity profiles move from dead to panic by increasing trade
pace, burstiness, whale probability, and daily volume.
To run the strategy analytics example:
npm run example -- strategy-backtestPanoptic v2 VM
The Panoptic v2 simulator currently ships inside uniswap-vm as the src/panoptic
public surface. That is intentional for now: the Panoptic VM depends on the same
offline Pool state and routes CLAMM effects through UniswapVmPanopticAdapter.
A separate panoptic-vm package can still be split later, but the current package
keeps the contract-parity simulator and underlying pool VM versioned together.
Panoptic state stays outside Pool. PanopticPoolVM owns option positions,
position balances, account hashes, settlement, collateral/risk hooks, and the
Panoptic account reporting surface. The underlying pool remains the CLAMM book;
all Panoptic liquidity effects pass through the adapter.
import {
FeeAmount,
PANOPTIC_V2_V3_REFERENCE_CONFIG,
PanopticPoolVM,
Pool,
UniswapVmPanopticAdapter,
addPanopticLeg,
addPoolId,
createPanopticPoolIdentity,
panopticV2V3ReferenceRiskConfig,
createToken
} from "@steerprotocol/clamm-vm/simulation";
const tokenA = createToken(1, "0x0000000000000000000000000000000000000001", 18, "A");
const tokenB = createToken(1, "0x0000000000000000000000000000000000000002", 18, "B");
const pool = Pool.fromTick({ tokenA, tokenB, fee: FeeAmount.MEDIUM, tickCurrent: 0 });
const identity = createPanopticPoolIdentity({
poolPattern: 0x12345n,
vegoid: PANOPTIC_V2_V3_REFERENCE_CONFIG.vegoid,
tickSpacing: 60,
minEnforcedTick: -887_220,
maxEnforcedTick: 887_220
});
const panoptic = new PanopticPoolVM({
marketId: "eth-usdc",
poolId: identity.poolId,
underlying: new UniswapVmPanopticAdapter(pool),
settlement, // implement PanopticPoolSettlementPort or use PanopticCollateralSettlement
risk: panopticV2V3ReferenceRiskConfig(),
clock: { timestamp: 1_717_171_717, blockNumber: 42 }
});
const tokenId = addPanopticLeg(addPoolId(0n, identity.poolId), {
index: 0,
optionRatio: 1,
asset: 0,
isLong: 0,
tokenType: 0,
riskPartner: 0,
strike: 600,
width: 2
});
panoptic.openPosition({
account: "alice",
tokenId,
positionSize: 1_000_000n,
tickAndSpreadLimits: { tickLimitLow: -887_220, tickLimitHigh: 887_220, effectiveLiquidityLimit: 10_000 }
});Panoptic reporting is fidelity-labeled. Level 2 state/risk fields are marked
decision-grade only when they come from PanopticPoolVM, CollateralTracker, or
RiskEngine read models. VM-owned checkpoint/rollback closeout reports can support
decision-grade NAV only when the runtime declares its settlement, collateral,
risk, oracle, and interest sources complete. Incomplete runtimes fail closed and
retain realized/accounting value only as a secondary, non-decision-grade surface.
To run the Panoptic example:
npm run example -- panoptic-backtestTo render a Panoptic VM activity GIF:
npm run example -- panoptic-gifTo rerun the pinned long-straddle burn vectors against the sibling
panoptic-v2-core checkout:
npm run test:panoptic-core-parityThe source-side command defaults to ../panoptic-v2-core, verifies the pinned
core commit, and uses that checkout's Foundry RPC configuration. Set
PANOPTIC_V2_CORE_PATH to select another checkout. The corresponding local VM
comparison is part of the normal Vitest suite and does not require an RPC.
It writes examples/output/panoptic.gif. The renderer progressively joins
source-owned market metrics, strategy portfolio rows, Panoptic snapshots, and
actual strategy action outcomes. It shows decision-grade closeout NAV against an
exact passive-hold benchmark, excess value, premium and fee attribution, hedge
inventory, market flow, active liquidity, pool price, and the open-position
structure. Open, close, hedge, and settlement actions retain their source timing;
routine actions are sampled for frame timing while the action lane keeps their
actual counts. The payoff curve remains explicitly diagnostic. If closeout NAV or
the passive benchmark is not decision-grade, the renderer does not present the
fallback accounting surface as NAV or PnL.
