Risk
Risk primitives answer protocol-specific exposure questions that don’t generalize across AMM families. Two primitives:
CheckTickRangeStatus— V3-only: where does the current price sit inside a position’s tick range?AssessDepegRisk— Stableswap-only: how much of a position is exposed to a stablecoin depeg?
By design these are protocol-specific. The category is not a “missing-coverage” gap — these questions only make sense in their respective protocol.
All primitives in the Agentic Primitives section follow the same contract: stateless construction, computation at .apply(), typed dataclass return.
from defipy.twin import MockProvider, StateTwinBuilder
provider = MockProvider()builder = StateTwinBuilder()lp_v3 = builder.build(provider.snapshot("eth_dai_v3"))lp_sts = builder.build(provider.snapshot("usdc_dai_stableswap_A10"))sts_tokens = lp_sts.factory.token_from_exchange[lp_sts.name]CheckTickRangeStatus
Section titled “CheckTickRangeStatus”Purpose. Report where the current pool price sits inside a V3 position’s tick range — “is my range about to go out of range?”
Signature.
CheckTickRangeStatus().apply(lp, lwr_tick, upr_tick) -> TickRangeStatusV2 raises ValueError. Ticks need not align to tick_spacing (real positions do, but the primitive accepts any valid tick in [MIN_TICK, MAX_TICK]). pct_to_lower / pct_to_upper go negative when price has crossed that bound — a useful out-of-range signal.
from defipy import CheckTickRangeStatus
# Current ETH/DAI price = 100 → tick ≈ 46054.# Tight in-range position centered on current tick:result = CheckTickRangeStatus().apply(lp_v3, lwr_tick=45000, upr_tick=47000)print(f"current_tick: {result.current_tick}")print(f"lower / upper: {result.lower_tick} / {result.upper_tick}")print(f"in_range: {result.in_range}")print(f"pct_to_lower: {result.pct_to_lower:.6f}")print(f"pct_to_upper: {result.pct_to_upper:.6f}")print(f"range_width_pct: {result.range_width_pct:.6f}")AssessDepegRisk
Section titled “AssessDepegRisk”Purpose. Quantify a stableswap position’s exposure across N depeg scenarios (analytical, no simulation).
Signature.
AssessDepegRisk().apply( lp, lp_init_amt, depeg_token, depeg_levels=None, compare_v2=True,) -> DepegRiskAssessmentdepeg_token is a token object from the pool’s factory. Default depeg_levels = [0.02, 0.05, 0.10, 0.20, 0.50]. When a depeg level is unreachable for the pool’s A, that scenario’s lp_value_at_depeg / il_pct come back as None (the V2 comparison is still populated, since V2 has no reachability bound).
from defipy import AssessDepegRisk
result = AssessDepegRisk().apply( lp_sts, lp_init_amt = 100.0, depeg_token = sts_tokens["USDC"],)print(f"depeg_token: {result.depeg_token}")print(f"protocol_type / n_assets: {result.protocol_type} / {result.n_assets}")print(f"current_peg_deviation: {result.current_peg_deviation}")print()print(f"{'depeg':>8} {'il_pct':>12} {'lp_value':>12} {'v2_il':>12}")for s in result.scenarios: il = f"{s.il_pct:.6f}" if s.il_pct is not None else "None" val = f"{s.lp_value_at_depeg:.4f}" if s.lp_value_at_depeg is not None else "None" v2 = f"{s.v2_il_comparison:.6f}" if s.v2_il_comparison is not None else "None" print(f"{s.depeg_pct*100:>7.0f}% {il:>12} {val:>12} {v2:>12}")Protocol coverage
Section titled “Protocol coverage”| Protocol | Supported | Notes |
|---|---|---|
| Uniswap V2 | ❌ | No risk primitive in this category — V2 is range-less and pegs are not relevant |
| Uniswap V3 | ✅ | CheckTickRangeStatus only |
| Balancer | ❌ | No risk primitive in v1 |
| Stableswap | ✅ | AssessDepegRisk only |
MCP tool exposure
Section titled “MCP tool exposure”In the curated 10:
AssessDepegRisk
Not in the curated 10:
CheckTickRangeStatus— high-traffic V3 question but easy to compose LLM-side fromlp.slot0.tickand the position’s savedlwr_tick/upr_tick.