@@ -78,6 +78,12 @@ access(all) struct SimConfig {
7878 access (all ) let maxHealth : UFix64
7979 /// Initial effective HF to coerce the position to at creation (matches Python initial_hf)
8080 access (all ) let initialHF : UFix64
81+ /// MOET:YT pool TVL in USD (from fixture's moet_yt.size)
82+ access (all ) let moetYtPoolTVL : UFix64
83+ /// MOET:YT pool TVL fraction — fraction of TVL placed as liquidity (0.95 = 95%)
84+ access (all ) let moetYtPoolTVLFraction : UFix64
85+ /// MOET:YT pool tick range — ±ticks from peg (100 = Python sim default)
86+ access (all ) let moetYtPoolTickRange : Int
8187
8288 init (
8389 prices : [UFix64 ],
@@ -90,7 +96,10 @@ access(all) struct SimConfig {
9096 minHealth : UFix64 ,
9197 targetHealth : UFix64 ,
9298 maxHealth : UFix64 ,
93- initialHF : UFix64
99+ initialHF : UFix64 ,
100+ moetYtPoolTVL : UFix64 ,
101+ moetYtPoolTVLFraction : UFix64 ,
102+ moetYtPoolTickRange : Int
94103 ) {
95104 self .prices = prices
96105 self .tickIntervalSeconds = tickIntervalSeconds
@@ -103,6 +112,9 @@ access(all) struct SimConfig {
103112 self .targetHealth = targetHealth
104113 self .maxHealth = maxHealth
105114 self .initialHF = initialHF
115+ self .moetYtPoolTVL = moetYtPoolTVL
116+ self .moetYtPoolTVLFraction = moetYtPoolTVLFraction
117+ self .moetYtPoolTickRange = moetYtPoolTickRange
106118 }
107119}
108120
@@ -151,16 +163,21 @@ fun setup() {
151163 signer : coaOwnerAccount
152164 )
153165
154- // MOET:morphoVault (yield token pool)
155- setPoolToPrice (
166+ // MOET:morphoVault (yield token pool) — finite liquidity matching Python sim
167+ let moetYtPool = simulation_flash_crash_moderate_pools [" moet_yt" ]!
168+ setPoolToPriceWithTVL (
156169 factoryAddress : factoryAddress ,
157170 tokenAAddress : moetAddress ,
158171 tokenBAddress : morphoVaultAddress ,
159172 fee : 100 ,
160173 priceTokenBPerTokenA : 1.0 ,
161174 tokenABalanceSlot : moetBalanceSlot ,
162175 tokenBBalanceSlot : fusdevBalanceSlot ,
163- signer : coaOwnerAccount
176+ signer : coaOwnerAccount ,
177+ tvl : moetYtPool .size ,
178+ tickRange : 100 ,
179+ tvlFraction : moetYtPool .concentration ,
180+ tokenBPriceUSD : 1.0
164181 )
165182
166183 // MOET:PYUSD0 (routing pool)
@@ -175,7 +192,7 @@ fun setup() {
175192 signer : coaOwnerAccount
176193 )
177194
178- // PYUSD0:WBTC (collateral/liquidation pool) — infinite liquidity for now
195+ // PYUSD0:WBTC (collateral/liquidation pool) — infinite liquidity
179196 let initialBtcPrice = simulation_flash_crash_moderate_prices [0 ]
180197 setPoolToPrice (
181198 factoryAddress : factoryAddress ,
@@ -225,14 +242,15 @@ access(all) fun ytPriceAtTick(_ tick: Int, tickIntervalSeconds: UFix64, yieldAPR
225242 return 1.0 + yieldAPR * (elapsedSeconds / secondsPerYear )
226243}
227244
228- /// Update all prices for a given simulation tick.
245+ /// Update oracle, collateral pool, and vault share price each tick.
246+ /// Does NOT touch the MOET/FUSDEV pool — that's managed by the arb bot reset.
229247access (all ) fun applyPriceTick (btcPrice : UFix64 , ytPrice : UFix64 , signer : Test .TestAccount ) {
230248 setBandOraclePrices (signer : bandOracleAccount , symbolPrices : {
231249 " BTC" : btcPrice ,
232250 " USD" : 1.0
233251 })
234252
235- // PYUSD0:WBTC pool — update BTC price
253+ // PYUSD0:WBTC pool — update BTC price (infinite liquidity)
236254 setPoolToPrice (
237255 factoryAddress : factoryAddress ,
238256 tokenAAddress : wbtcAddress ,
@@ -244,18 +262,6 @@ access(all) fun applyPriceTick(btcPrice: UFix64, ytPrice: UFix64, signer: Test.T
244262 signer : coaOwnerAccount
245263 )
246264
247- // MOET:FUSDEV pool — update YT price
248- setPoolToPrice (
249- factoryAddress : factoryAddress ,
250- tokenAAddress : moetAddress ,
251- tokenBAddress : morphoVaultAddress ,
252- fee : 100 ,
253- priceTokenBPerTokenA : UFix128 (ytPrice ),
254- tokenABalanceSlot : moetBalanceSlot ,
255- tokenBBalanceSlot : fusdevBalanceSlot ,
256- signer : coaOwnerAccount
257- )
258-
259265 setVaultSharePrice (
260266 vaultAddress : morphoVaultAddress ,
261267 assetAddress : pyusd0Address ,
@@ -267,6 +273,25 @@ access(all) fun applyPriceTick(btcPrice: UFix64, ytPrice: UFix64, signer: Test.T
267273 )
268274}
269275
276+ /// Arb bot simulation: reset MOET/FUSDEV pool to peg with finite TVL.
277+ /// Called after all agents trade each tick.
278+ access (all ) fun resetYieldPoolToFiniteTVL (ytPrice : UFix64 , tvl : UFix64 , tvlFraction : UFix64 , tickRange : Int ) {
279+ setPoolToPriceWithTVL (
280+ factoryAddress : factoryAddress ,
281+ tokenAAddress : moetAddress ,
282+ tokenBAddress : morphoVaultAddress ,
283+ fee : 100 ,
284+ priceTokenBPerTokenA : UFix128 (ytPrice ),
285+ tokenABalanceSlot : moetBalanceSlot ,
286+ tokenBBalanceSlot : fusdevBalanceSlot ,
287+ signer : coaOwnerAccount ,
288+ tvl : tvl ,
289+ tickRange : tickRange ,
290+ tvlFraction : tvlFraction ,
291+ tokenBPriceUSD : 1.0
292+ )
293+ }
294+
270295// ============================================================================
271296// SIMULATION RUNNER
272297// ============================================================================
@@ -316,8 +341,6 @@ access(all) fun runSimulation(config: SimConfig, label: String): SimResult {
316341 let vaultId = yieldVaultIDs [0 ]
317342
318343 // Step 1: Coerce position to the desired initial HF.
319- // Set temporary health params with targetHealth=initialHF, then force-rebalance.
320- // This makes the on-chain rebalancer push the position to exactly initialHF.
321344 setPositionHealth (
322345 signer : flowALPAccount ,
323346 pid : pid ,
@@ -379,56 +402,62 @@ access(all) fun runSimulation(config: SimConfig, label: String): SimResult {
379402 highestPrice = absolutePrice
380403 }
381404
382- if absolutePrice ! = previousBTCPrice {
383- let expectedTimestamp = startTimestamp + UFix64 (step ) * config .tickIntervalSeconds
384- let currentTimestamp = getCurrentBlockTimestamp ()
385- if expectedTimestamp > currentTimestamp {
386- Test .moveTime (by : Fix64 (expectedTimestamp - currentTimestamp ))
387- }
405+ if absolutePrice == previousBTCPrice {
406+ step = step + 1
407+ continue
408+ }
409+
410+ let expectedTimestamp = startTimestamp + UFix64 (step ) * config .tickIntervalSeconds
411+ let currentTimestamp = getCurrentBlockTimestamp ()
412+ if expectedTimestamp > currentTimestamp {
413+ Test .moveTime (by : Fix64 (expectedTimestamp - currentTimestamp ))
414+ }
388415
389- applyPriceTick (btcPrice : absolutePrice , ytPrice : ytPrice , signer : users [0 ])
416+ applyPriceTick (btcPrice : absolutePrice , ytPrice : ytPrice , signer : users [0 ])
390417
391- // Calculate HF BEFORE rebalancing
392- var preRebalanceHF : UFix64 = UFix64 (getPositionHealth (pid : pids [0 ], beFailed : false ))
418+ // Calculate HF BEFORE rebalancing
419+ var preRebalanceHF : UFix64 = UFix64 (getPositionHealth (pid : pids [0 ], beFailed : false ))
393420
394- // Rebalance agents on interval
395- if config .rebalanceInterval < = 1 || step % config .rebalanceInterval == 0 {
396- var a = 0
397- while a < config .numAgents {
398- rebalanceYieldVault (signer : flowYieldVaultsAccount , id : vaultIds [a ], force : false , beFailed : false )
399- rebalancePosition (signer : flowALPAccount , pid : pids [a ], force : false , beFailed : false )
400- a = a + 1
401- }
421+ // Rebalance agents sequentially — each swap moves pool price for next agent
422+ if config .rebalanceInterval < = 1 || step % config .rebalanceInterval == 0 {
423+ var a = 0
424+ while a < config .numAgents {
425+ rebalanceYieldVault (signer : flowYieldVaultsAccount , id : vaultIds [a ], force : false , beFailed : false )
426+ rebalancePosition (signer : flowALPAccount , pid : pids [a ], force : false , beFailed : false )
427+ a = a + 1
402428 }
403429
404- // Count actual rebalances that occurred this tick
405- let currentVaultRebalanceCount = Test .eventsOfType (Type <DeFiActions .Rebalanced >()).length
406- let currentPositionRebalanceCount = Test .eventsOfType (Type <FlowALPv0 .Rebalanced >()).length
407- let tickVaultRebalances = currentVaultRebalanceCount - prevVaultRebalanceCount
408- let tickPositionRebalances = currentPositionRebalanceCount - prevPositionRebalanceCount
409- prevVaultRebalanceCount = currentVaultRebalanceCount
410- prevPositionRebalanceCount = currentPositionRebalanceCount
430+ // Arb bot: reset MOET:FUSDEV pool to peg with finite TVL
431+ resetYieldPoolToFiniteTVL (ytPrice : ytPrice , tvl : config .moetYtPoolTVL , tvlFraction : config .moetYtPoolTVLFraction , tickRange : config .moetYtPoolTickRange )
432+ }
411433
412- // Calculate HF AFTER rebalancing
413- var postRebalanceHF : UFix64 = UFix64 (getPositionHealth (pid : pids [0 ], beFailed : false ))
434+ // Count actual rebalances that occurred this tick
435+ let currentVaultRebalanceCount = Test .eventsOfType (Type <DeFiActions .Rebalanced >()).length
436+ let currentPositionRebalanceCount = Test .eventsOfType (Type <FlowALPv0 .Rebalanced >()).length
437+ let tickVaultRebalances = currentVaultRebalanceCount - prevVaultRebalanceCount
438+ let tickPositionRebalances = currentPositionRebalanceCount - prevPositionRebalanceCount
439+ prevVaultRebalanceCount = currentVaultRebalanceCount
440+ prevPositionRebalanceCount = currentPositionRebalanceCount
414441
415- // Track lowest HF (use pre-rebalance to capture the actual low point)
416- if preRebalanceHF < lowestHF && preRebalanceHF > 0.0 {
417- lowestHF = preRebalanceHF
418- }
442+ // Calculate HF AFTER rebalancing
443+ var postRebalanceHF : UFix64 = UFix64 (getPositionHealth (pid : pids [0 ], beFailed : false ))
419444
420- // Log every tick with pre->post HF
421- log (" [t=\( step ) ] price=$\( absolutePrice ) yt=\( ytPrice ) HF=\( preRebalanceHF ) ->\( postRebalanceHF ) vaultRebalances=\( tickVaultRebalances ) positionRebalances=\( tickPositionRebalances ) " )
445+ // Track lowest HF (use pre-rebalance to capture the actual low point)
446+ if preRebalanceHF < lowestHF && preRebalanceHF > 0.0 {
447+ lowestHF = preRebalanceHF
448+ }
422449
423- // Liquidation check (pre-rebalance HF is the danger point)
424- if preRebalanceHF < 1.0 && preRebalanceHF > 0.0 {
425- liquidationCount = liquidationCount + 1
426- log (" *** LIQUIDATION agent=0 at t=\( step ) ! HF=\( preRebalanceHF ) ***" )
427- }
450+ // Log every tick with pre->post HF
451+ log (" [t=\( step ) ] price=$\( absolutePrice ) yt=\( ytPrice ) HF=\( preRebalanceHF ) ->\( postRebalanceHF ) vaultRebalances=\( tickVaultRebalances ) positionRebalances=\( tickPositionRebalances ) " )
428452
429- previousBTCPrice = absolutePrice
453+ // Liquidation check (pre-rebalance HF is the danger point)
454+ if preRebalanceHF < 1.0 && preRebalanceHF > 0.0 {
455+ liquidationCount = liquidationCount + 1
456+ log (" *** LIQUIDATION agent=0 at t=\( step ) ! HF=\( preRebalanceHF ) ***" )
430457 }
431458
459+ previousBTCPrice = absolutePrice
460+
432461 step = step + 1
433462 }
434463
@@ -487,16 +516,19 @@ fun test_FlashCrashModerate_ZeroLiquidations() {
487516 let result = runSimulation (
488517 config : SimConfig (
489518 prices : simulation_flash_crash_moderate_prices ,
490- tickIntervalSeconds : 5 .0 ,
491- numAgents : 5 ,
519+ tickIntervalSeconds : 60 .0 , // 1 min per tick, matching Python sim
520+ numAgents : 10 ,
492521 fundingPerAgent : 1.0 ,
493522 yieldAPR : simulation_flash_crash_moderate_constants .yieldAPR ,
494523 expectedLiquidationCount : 0 ,
495- rebalanceInterval : 1 ,
524+ rebalanceInterval : 5 , // every 5 ticks
496525 minHealth : 1.05 ,
497526 targetHealth : 1.08 ,
498527 maxHealth : UFix64.max , // Python sim has no upper health bound
499- initialHF : 1.15
528+ initialHF : 1.15 ,
529+ moetYtPoolTVL : simulation_flash_crash_moderate_pools [" moet_yt" ]! .size ,
530+ moetYtPoolTVLFraction : simulation_flash_crash_moderate_pools [" moet_yt" ]! .concentration ,
531+ moetYtPoolTickRange : 100 // Python sim: ±100 ticks (~1% range)
500532 ),
501533 label : " FlashCrashModerate"
502534 )
0 commit comments