Skip to content

AnalyzeBalancerPosition

AnalyzeBalancerPosition is the read-only, stateless primitive for 2-asset Balancer weighted pools. Sibling to AnalyzePosition (V2/V3) and AnalyzeStableswapPosition — same answer shape, adapted to Balancer’s weighted IL where the weight (not just the price ratio) drives IL.

The Balancer entry point that lets AggregatePortfolio route mixed-protocol portfolios.

ProtocolRequired call shape
Uniswap V2❌ Use AnalyzePosition
Uniswap V3❌ Use AnalyzePosition
BalancerAnalyzeBalancerPosition().apply(lp, lp_init_amt, entry_base_amt, entry_opp_amt, holding_period_days=None)
Stableswap❌ Use AnalyzeStableswapPosition
ParameterTypeDescription
lpBalancerExchange2-asset Balancer pool. N>2 raises ValueError (propagated from BalancerImpLoss).
lp_init_amtfloatPool shares held by the position.
entry_base_amtfloatAmount of base token deposited at entry (first token in pool insertion order).
entry_opp_amtfloatAmount of opp token deposited at entry.
holding_period_daysfloat (optional)Holding period in days; enables real_apr annualization.

Balancer weighted-pool IL formula (with base-token weight w):

IL(α)=αw+(1w)αw11,α=pcurrentpentry\mathrm{IL}(\alpha) = \alpha^{w} + (1 - w) \cdot \alpha^{w - 1} - 1, \qquad \alpha = \frac{p_{current}}{p_{entry}}

where p is opp-per-base. At w = 0.5 this collapses to the V2 formula; at other weights the curve is asymmetric. Composed via BalancerImpLoss — the sibling-repo helper, lifted there during the 1.2.0 work symmetric with UniswapImpLoss and StableswapImpLoss.

Numeraire: opp-token (the second token in the pool’s insertion order). Differs from AnalyzePosition’s token0 numeraire — callers aggregating across protocols in a common token need to rebase manually. AggregatePortfolio handles this by requiring a shared first-token symbol across positions.

Alpha computed fee-free. The primitive goes direct to reserves rather than through lp.get_price() which bakes in a fee scale factor — keeps the IL pure to price divergence and consistent with the IL formula’s derivation.

Scope: 2-asset only. Inherited from BalancerImpLoss’s own scope. N-asset extension requires first extending BalancerImpLoss.

No fee attribution in v1. Balancer’s collected_fees is vault-level with no per-LP attribution inside the pool object — surfacing a derived fee number would fabricate precision the state doesn’t carry. fee_income = 0.0 always; il_with_fees == il_percentage.

Diagnosis enum has only two values in v1: "net_positive" and "il_dominant". When fee attribution lands, "fee_compensated" will be added to match AnalyzePosition’s shape.

from defipy import AnalyzeBalancerPosition
from defipy.twin import MockProvider, StateTwinBuilder
provider = MockProvider()
builder = StateTwinBuilder()
lp_bal = builder.build(provider.snapshot("eth_dai_balancer_50_50"))
# Same scenario as the V2 example: ETH entered at 80 DAI, now 100 DAI.
result = AnalyzeBalancerPosition().apply(
lp_bal,
lp_init_amt = 100.0,
entry_base_amt = 1000.0,
entry_opp_amt = 80000.0,
holding_period_days = 30.0,
)
print(f"base / opp: {result.base_tkn_name} / {result.opp_tkn_name}")
print(f"base_weight: {result.base_weight}")
print(f"current_value: {result.current_value:.4f}")
print(f"hold_value: {result.hold_value:.4f}")
print(f"il_percentage: {result.il_percentage:.6f}")
print(f"net_pnl: {result.net_pnl:.4f}")
print(f"alpha: {result.alpha:.6f}")
print(f"diagnosis: {result.diagnosis}")
base / opp: ETH / DAI base_weight: 0.5 current_value: 200000.0000 hold_value: 180000.0000 il_percentage: -0.006192 net_pnl: 20000.0000 alpha: 1.250000 diagnosis: net_positive

At w = 0.5 the IL value matches the V2 reference (-0.006192). For w ≠ 0.5 the same α = 1.25 would produce a different IL value, asymmetric in direction.