Numerical Optimization — Part I: Introduction
Who Optimizes the Optimizers?
MEV searching boils down to a set of critical elements: pathfinding, numerical optimization, execution.
Numerical optimization is a broad category concerning the question “what is the optimal input that satisfies the constraints of my problem?”
This question is highly relevant to us. After identifying a price mismatch across some asset market, we must determine the most profitable route to capture value and how to size the associated trades.
I first covered numerical optimization with a lesson covering the Brent solver implemented in SciPy.
Later I focused on convex optimization:
Playing To Your Strengths
It’s important to understand your strengths and weaknesses, and even more important to choose efforts that are helped by your strengths, not harmed by your weaknesses, or ideally both.
I’m above average at abstract thinking, verbal reasoning, working memory, and synthesis (applying techniques from multiple domains). But I’m below average at mental processing speed, spatial reasoning, and numeric intuition.
This sets me up for trouble when I’m faced with certain classes of problems, and it compounds if that problem has multiple non-obvious solutions.
When I encounter some numerical optimization problem, I often have little intuition about how to attack it, falling back on brute-force methods and taking the first solution that works without spending much time on it.
Luckily, I now have access to highly capable AI tools that can fill in these gaps.
I already showed how to use AI to boost feature development and debugging.
Then I showed how to use it to quickly iterate on developing smart contracts.
Now I’m going to demonstrate how I’m using it to refine parts of my code base with methods that would be tedious to apply by hand.
This will be a multi-part series because the scope is too broad for a single entry. Even after I reach a reasonable stopping point, I may continue adding new entries as I discover and implement new techniques.
Problem & Test Setup
We need a way to compare methods across runs, so we need to build a reproducible set of inputs that model a relevant problem in our domain.
Since the majority of our work concerns liquidity pools from Uniswap and its various forks, we need a way to generate synthetic pool states associated with a given arbitrage scenario. I gave this to an agent, which ultimately proposed this architecture:
┌─────────────────────────────────────────────────────────────────────┐
│ FixtureFactory │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ PoolStateGenerator │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐│ │
│ │ │ V2 Pools │ │ V3 Pools │ │ V4 Pools ││ │
│ │ │ (reserves) │ │(tick-based) │ │(pool ID + tick-based) ││ │
│ │ └─────────────┘ └─────────────┘ └─────────────────────────┘│ │
│ └───────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
│
▼
┌──────────────────────┐
│ ArbitrageCycleFixture│
│ (frozen dataclass) │
└──────────────────────┘I’m omitting considerable detail on how this works under the hood, but the takeaway is that we can use this setup to mix and match arbitrage scenarios across the different pool types.
Scenario: Basic Two-Pool
Generate a basic scenario using two Uniswap V2 pools with a fixed price ratio and a target liquidity:
def simple_v2_arb_profitable(self) -> ArbitrageCycleFixture:
"""
Two V2 pools with 2% price difference.
Simple profit: arbitrage from higher to lower price pool.
Pool A: 1 ETH = 2000 USDC (price = 2000)
Pool B: 1 ETH = 1960 USDC (price = 1960, 2% lower)
Arbitrage: Buy ETH in pool B (cheaper), sell in pool A (more expensive).
"""
pool_a_address: ChecksumAddress = cast(
"ChecksumAddress", "0x0000000000000000000000000000000000000001"
)
pool_b_address: ChecksumAddress = cast(
"ChecksumAddress", "0x0000000000000000000000000000000000000002"
)
input_token_address: ChecksumAddress = cast(
"ChecksumAddress", "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
) # USDC
# Generate pools with 2% price difference
pool_a, pool_b = self._generator.generate_profitable_v2_pair(
pool_a_address=pool_a_address,
pool_b_address=pool_b_address,
fee_a=Fraction(3, 1000),
fee_b=Fraction(3, 1000),
price_ratio=1.02,
liquidity_base=10**21, # ~1000 ETH equivalent
)
return ArbitrageCycleFixture(
id="simple_v2_arb_profitable",
cycle_type="v2_v2",
pool_states={pool_a_address: pool_a, pool_b_address: pool_b},
input_token_address=input_token_address,
expected_optimal_input=0, # Calculated by solver
expected_profit=0, # Calculated by solver
)SciPy Benchmark
Now let’s compare the three methods implemented by SciPy’s minimize_scalar solver (Bounded, Brent, and Golden):




