Skip to content

Repository files navigation

Solver Inventory Replay

This repo replays Ethereum Rizzolver WETH/USDC settlement movements through an Avellaneda-style inventory strategy.

For an explanation of the workflow, addresses, current results, and the proposed reward-aware negative rebalancing extension, see:

docs/rebalancing_strategy_report.md

Quick Links

Tile Link
📄 Report Solver inventory rebalancing investigation
🧪 Fixture Ethereum Rizzolver two-day fixture
📚 Paper Avellaneda and Stoikov paper
🔎 Solver CoW Explorer Rizzolver page
📈 Tuning Rizzolver tuning snapshot

Useful references:

Committed fixture data:

  • Ethereum Rizzolver two-day fixture covers 2026-06-09T14:02:00Z -> 2026-06-11T14:11:00Z with 571 loaded settlement events and 2,890 one-minute ETHUSDC candles. It includes both the original default summary and the tuned reward-aware Rizzolver summary. Longer validation runs should be generated locally under ignored data/ and results/.

TL;DR

The original 1.0 WETH tuned reward-aware Avellaneda strategy is not ready as an always-on standalone rebalancer. A new 90-day exposure-control rerun shows the drift is not inevitable: with larger Avellaneda capacity, the same 46% start / 40% target case finishes close to target instead of ending near 96% WETH.

Decision question Answer
90-day exposure-control fix 90-day 46% start / 40% target, 25 WETH Avellaneda orders: final 40.0538% WETH versus a 40% target. Use this as exposure-control evidence only; the replay PnL is not production-grade.
Middle order-size check 10 WETH already keeps targets close: 40.1294% final WETH for the 40% target and 50.1139% for the 50% target. 15 WETH is a stronger middle compromise before jumping to 25 WETH.
50/50 sensitivity check 90-day 50% start / 50% target, 25 WETH Avellaneda orders: final 50.0590% WETH versus a 50% target. The original 1 WETH profile still drifts to 96.0796% WETH.
Whole scenario table See Scenario Summary for every tested scenario, including order size, configured Avellaneda parameters, value result, and final WETH versus target.
Best original-size value result 90-day 46% start / 40% target, 1 WETH Avellaneda orders: +2.8537% / +1,456,162.01 USDC, but it ends at 95.7981% WETH, so it fails exposure control.
Best lower-capacity target-keeping result Low WETH-flow pressure: +0.6400% / +338,863.14 USDC versus settlement-only, ending at 41.7956% WETH versus a 40% target.
Best neutral-price evidence Stable endpoint: +0.8684% / +458,971.41 USDC, ending at 43.6727% WETH. This shows Avellaneda can repair inventory when ETH has no strong final price trend.
Main failure mode When Rizzolver receives too much WETH, static 1.0 WETH Avellaneda orders are too small. The controlled 90-day rerun fixes this by increasing order size and adding inventory-risk reduction credit.
Production requirement Add liquidity/fill validation, dynamic order sizing, hard exposure caps, regime gates, and/or a separate hedge venue before relying on this strategy.

Practical reading: do not choose the original most profitable run if the goal is to stay market-neutral. The new controlled 90-day run keeps the target, but its very large PnL comes from a one-minute high/low fill model with large orders. Use it as evidence that Avellaneda needs enough capacity to offset Rizzolver flow, then validate liquidity, queueing, slippage, and hard exposure limits before production.

Reward-aware versus non-reward-aware should only be compared apples-to-apples: same time window, same initial inventory, same target percentage, same order size, same risk factor, same spread, and same fee setting.

Latest local validation:

  • A two-day Rizzolver reward-aware run over 2026-06-09T14:02:00Z -> 2026-06-11T14:11:00Z loaded 571 settlement events. Estimated Rizzolver reward was 13,297.22 USDC, but the tuned reward-aware profile was still worse than settlement-only by -22,341.73 USDC.
  • A fresh two-week Rizzolver run over 2026-05-29T06:55:00Z -> 2026-06-12T08:21:00Z loaded 966 settlement events. Estimated Rizzolver reward was 20,624.38 USDC, but the tuned reward-aware profile was still worse than settlement-only by -104,643.93 USDC; rewards covered 19.7091% of the Avellaneda loss. See Two-Week Rizzolver Validation Run.
  • A 90-day sender-prefiltered Rizzolver run over 2026-03-14T03:43:00Z -> 2026-06-12T11:21:00Z loaded 32,699 settlement events. The original 46% start / 40% target run was better than settlement-only by +1,456,162.01 USDC but ended at 95.7981% WETH. The controlled rerun with order_amount=25, risk_factor=3, min_spread=0.2, and inventory_risk_reduction_weight=0.1 ended at 40.0538% WETH. See Ninety-Day Rizzolver Validation Run.
  • The same 90-day dataset was also tested with a 50% WETH / 50% USDC start and a 50% WETH target. The original 1 WETH profile ended at 96.0796% WETH, while the controlled 25 WETH profile ended at 50.0590%. See Ninety-Day Exposure-Control Tuning.
  • Additional stress checks were added without changing the baseline runs: price-regime windows for ETH up (+19.8061%), ETH down (-26.5564%), and stable endpoint (+0.0035%); high-flow and low-flow inventory-pressure windows; and additional activity/volatility/adverse-flow scenarios. See Price-Regime Stress Checks, Inventory-Flow Stress Checks, and Additional Scenario Checks.

The idea is:

flowchart LR
    A["CoW solver submits settlement transactions"]
    B["CoW GPv2Settlement moves WETH/USDC"]
    C["Extract WETH/USDC deltas"]
    D["Inject deltas into simulated portfolio"]
    E["Avellaneda reads changed inventory"]
    F["Strategy trades back toward target inventory"]

    A --> B --> C --> D --> E --> F
Loading

Text version:

CoW solver txs -> GPv2Settlement WETH/USDC movement -> extracted deltas -> simulated portfolio -> Avellaneda rebalance

The main question is:

Does Avellaneda improve the result after solver settlements move inventory,
or would it be better to just accept the settlement-driven inventory changes?

Current replay metrics can include Rizzolver reward estimates from the CoW competition API. Use --reward-aware to make those estimates act as a budget for locally negative rebalancing trades. These are per-auction performance-reward estimates, not final weekly COW payouts. The model does not simulate claiming or selling COW tokens; it treats estimated rewards as an immediately available USDC-equivalent budget.

The 90-day controlled rerun also uses inventory_risk_reduction_weight=0.1. That is an inventory-risk credit, not realized reward income. It means the controlled result is reward-aware plus inventory-risk-aware, and should be validated with real fill quality and liquidity assumptions before production.

Main Command

Run everything in one command:

cd /Users/riccardopersiani/Documents/solver-inventory-replay

python3 run_fetch_and_replay.py \
  --chain ethereum \
  --match-mode cow-api \
  --solver-address 0x8f5835e9d756c9bd934bce527157a4b0ef3c5cb7 \
  --lookback-blocks 50400 \
  --max-auctions 60000 \
  --cow-api-workers 16 \
  --initial-base 10000 \
  --initial-quote 30000000 \
  --inventory-target-base-pct 40 \
  --order-amount 1.0 \
  --risk-factor 3.0 \
  --min-spread 0.2 \
  --add-transaction-costs \
  --reward-aware \
  --volatility-buffer-size 200 \
  --trading-intensity-buffer-size 200 \
  --out-dir results/ethereum_rizzolver_replay

This does five things:

1. Resolve the Ethereum block/date range.
2. Fetch Rizzolver CoW WETH/USDC settlement deltas for that period.
3. Download ETH/USDC candles for the same period.
4. Replay solver settlements plus Avellaneda.
5. Write intermediate and final files into the output directory.

Output files:

results/ethereum_rizzolver_replay/settlement_events.csv
results/ethereum_rizzolver_replay/raw_transfers.csv
results/ethereum_rizzolver_replay/summary.json
results/ethereum_rizzolver_replay/trades.csv
results/ethereum_rizzolver_replay/equity_curve.csv
results/ethereum_rizzolver_replay/applied_settlements.csv
results/ethereum_rizzolver_replay/candles.csv

Check existing result folders:

python3 inspect_results.py

This lists each results/*/summary.json and shows:

settlement_only    = solver settlements only, no Avellaneda
strategy           = solver settlements plus Avellaneda
avellaneda_vs_settlement = strategy minus settlement-only
result             = better/worse/same for Avellaneda versus settlement-only

If an older summary is missing fields that can be derived from values already in the file, repair it with:

python3 inspect_results.py --fix-derived

Fetch Rizzolver settlement deltas only:

python3 fetch_cow_settlement_deltas.py \
  --chain ethereum \
  --match-mode cow-api \
  --solver-address 0x8f5835e9d756c9bd934bce527157a4b0ef3c5cb7 \
  --lookback-blocks 14400 \
  --max-auctions 18000 \
  --cow-api-workers 16 \
  --progress-every 5 \
  --tx-progress-every 100 \
  --checkpoint-every 500 \
  --out data/ethereum_8f5835e9_settlement_events_2d.csv \
  --raw-out data/ethereum_8f5835e9_raw_transfers_2d.csv

--chain ethereum uses:

RPC:        https://mainnet.gateway.tenderly.co/4EwJfXA56zTRLw9aQkvXxO
Settlement: 0x9008D19f58AAbD9eD0D60971565AA8510560ab41  (Ethereum mainnet CoW GPv2Settlement)
WETH:       0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
USDC:       0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48

For Ethereum, --lookback-blocks 14400 is approximately two days, 50400 is approximately seven days, and 100800 is approximately fourteen days. When using --match-mode cow-api, set --max-auctions high enough to cover the block window because many auction IDs can be missing.

--match-mode cow-api is the default and should be used when --solver-address is a CoW solver identity address.

Why this matters:

CoW solverAddress = official solver identity in CoW competition data.
tx.from           = on-chain submission address that sends the settlement tx.

Those can be different. If you use --match-mode tx-from, the script only finds transactions where tx.from exactly equals --solver-address. That can miss real solver settlements.

Use --match-mode tx-from only when you intentionally pass an on-chain settlement submission address.

What The Replay Compares

The replay tracks three paths.

Hold Only

Start with initial WETH + USDC.
Do not apply solver settlements.
Do not run Avellaneda.
Mark the initial balances at the final ETH/USDC price.

Summary fields:

hold_only_pnl_pct
buy_and_hold_pnl_pct
buy_and_hold_pnl_quote

Settlement Only

Start with initial WETH + USDC.
Apply solver settlement WETH/USDC deltas.
Do not run Avellaneda.
Mark the adjusted balances at the final ETH/USDC price.

Summary fields:

settlement_only_pnl_pct
settlement_only_pnl_quote
settlement_only_value_quote
adjusted_base_pct
settlement_only_base_pct

Settlement + Avellaneda

Start with initial WETH + USDC.
Apply solver settlement WETH/USDC deltas.
Let Avellaneda trade after those external inventory movements.
Mark the final balances at the final ETH/USDC price.

Summary fields:

pnl_pct
pnl_quote
final_value_quote
final_base_pct
base_pct_error

The key comparison is:

avellaneda_vs_settlement_only_quote

This means:

final value with settlements + Avellaneda
minus
final value with settlements only

Positive means Avellaneda helped versus doing nothing after solver settlements. Negative means Avellaneda made the result worse.

Important Summary Fields

solver_address

The solver address used for the settlement extraction.

solver_addresses

All solver addresses present in the settlement CSV. Usually this is one address.

initial_base_pct

Initial WETH percentage of the portfolio.

adjusted_base_pct

WETH percentage after applying solver settlements only, before Avellaneda impact.

final_base_pct

WETH percentage after solver settlements plus Avellaneda trades.

inventory_target_base_pct

Target WETH percentage configured for Avellaneda.

base_pct_error

final_base_pct - inventory_target_base_pct

Positive means final inventory is overweight WETH. Negative means final inventory is underweight WETH.

Derived inventory diagnostics:

settlement_only_imbalance_pct_points = settlement_only_base_pct - inventory_target_base_pct
avellaneda_rebalance_move_pct_points = final_base_pct - settlement_only_base_pct
imbalance_closed_pct =
  (abs(settlement_only_imbalance_pct_points) - abs(base_pct_error))
  / abs(settlement_only_imbalance_pct_points)
  * 100

settlement_only_imbalance_pct_points shows how unbalanced Rizzolver inventory looks without Avellaneda. imbalance_closed_pct shows what percentage of that imbalance Avellaneda corrected.

total_external_base_delta

Total WETH delta from solver settlements.

total_external_quote_delta

Total USDC delta from solver settlements.

fetch_metadata

The block and date range used by the one-command runner.

CoW Settlement Extraction

CoW solvers are not simple wallets that directly send/receive WETH and USDC in every settlement.

For CoW, the solver EOA usually submits a transaction to:

CoW Ethereum GPv2Settlement
0x9008D19f58AAbD9eD0D60971565AA8510560ab41

The ERC20 transfers happen through the settlement contract, vault relayer, and interaction targets. That is why this repo uses:

transaction sender = solver address
transaction touches GPv2Settlement
WETH/USDC Transfer logs inside the transaction receipt
only transfers involving GPv2Settlement

The sign convention is from the GPv2Settlement contract perspective:

weth_delta > 0  means GPv2Settlement received WETH
weth_delta < 0  means GPv2Settlement sent WETH
usdc_delta > 0  means GPv2Settlement received USDC
usdc_delta < 0  means GPv2Settlement sent USDC

This differs from direct solver-wallet inventory. It is a pragmatic settlement-flow proxy attributed to the solver transaction sender.

Separate Scripts

Fetch Settlements Only

python3 fetch_cow_settlement_deltas.py \
  --chain ethereum \
  --match-mode cow-api \
  --solver-address 0x8f5835e9d756c9bd934bce527157a4b0ef3c5cb7 \
  --lookback-blocks 14400 \
  --max-auctions 18000 \
  --cow-api-workers 16 \
  --out data/ethereum_rizzolver_settlement_events.csv \
  --raw-out data/ethereum_rizzolver_raw_transfers.csv

Normalized output:

timestamp,block_number,tx_hash,solver_address,weth_delta,usdc_delta,raw_transfer_count,auction_id,is_winner,ranking,orders_count,solver_score_wei,reference_score_wei,total_winning_score_wei,estimated_reward_native

Raw output:

timestamp,block_number,tx_hash,log_index,solver_address,token_symbol,token_address,from_address,to_address,amount_raw,amount,delta

Replay Existing Settlement CSV

python3 replay_with_avellaneda.py \
  --settlements data/ethereum_rizzolver_settlement_events.csv \
  --symbol ETHUSDC \
  --interval 1m \
  --start 2026-06-09 \
  --end now \
  --initial-base 10000 \
  --initial-quote 30000000 \
  --inventory-target-base-pct 40 \
  --order-amount 1.0 \
  --risk-factor 3.0 \
  --min-spread 0.2 \
  --add-transaction-costs \
  --reward-aware \
  --execution-timeframe-mode infinite \
  --volatility-buffer-size 200 \
  --trading-intensity-buffer-size 200 \
  --out-dir results/rizzolver_manual_replay

Use this if you already have settlement_events.csv and want to test different Avellaneda parameters without refetching chain data.

Avellaneda Parameters

--inventory-target-base-pct

Target WETH percentage. Example:

40 = 40% WETH / 60% USDC

--order-amount

WETH amount per Avellaneda simulated order.

--risk-factor

Avellaneda inventory risk parameter. Higher values push inventory more strongly toward the target but may reduce profitable fills.

--min-spread

Minimum total spread in percent. 0.05 means 0.05%, or 5 bps.

--volatility-buffer-size

Number of candles used to estimate volatility.

--trading-intensity-buffer-size

Number of candles used to approximate liquidity/trading intensity.

--execution-timeframe-mode

Use infinite for ongoing inventory rebalancing. Use from_date_to_date only if you want finite-horizon time decay over the replay window.

--reward-aware

Uses Rizzolver reward estimates from settlement_events.csv as a budget for locally negative rebalancing trades. This only works when the settlement CSV includes CoW API reward fields such as solver_score_wei, reference_score_wei, and estimated_reward_native.

Important caveat: this is an accounting model, not a COW-token treasury flow. It does not model reward claim timing, COW conversion liquidity, slippage, COW price risk, or claim/conversion gas costs.

Reward Fields

Reward metadata is stored at three levels:

settlement_events.csv    raw per-settlement CoW competition reward metadata
applied_settlements.csv  per-settlement reward converted into USDC at replay time
summary.json             replay-level reward totals and reward-budget outcome

Start with these fields:

Field File Meaning
estimated_reward_native settlement_events.csv Estimated per-settlement reward in native token units from CoW competition scores.
estimated_reward_quote applied_settlements.csv estimated_reward_native converted into USDC at the settlement candle price.
solver_reward_quote summary.json Total estimated Rizzolver reward in USDC over the replay.
reward_budget_after_avellaneda_quote summary.json solver_reward_quote + avellaneda_vs_settlement_only_quote; positive means rewards cover Avellaneda underperformance.
reward_coverage_ratio summary.json Share of the Avellaneda loss covered by estimated rewards when Avellaneda is worse than settlement-only.
reward_aware_skipped_trades summary.json Negative-edge trades skipped because reward budget was insufficient.

Full field definitions are documented in Reward Field Glossary.

Current Limitations

  • The replay uses candle-level ETH/USDC data, not full order-book replay.
  • Large historical scans can be slow, especially when fetching receipt-level settlement data.
  • ERC20 transfer logs show token movement, not full solver intent.
  • Transfers are measured around CoW GPv2Settlement, not a private solver balance sheet.
  • If settlements route through other assets, this first version only keeps WETH/USDC deltas.
  • If WETH is unwrapped to native ETH, native ETH movement is not captured by ERC20 transfer logs.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages