Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs/impermanent-loss.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Pool return estimates

`GET /pools` reports fee APR and impermanent loss together for a 24-hour
window. `netReturn24hPct` adds the estimated 24-hour fee return to the
divergence loss for the same window; all values are estimates, not a prediction
of future returns.

The divergence estimate assumes a constant-product 50/50 pool and compares the
first stored spot price in the last 24 hours with the latest stored spot price.
It does not include deposits, withdrawals, rewards, or gas. A two-point example
with a price moving from 1 to 2 has an estimated divergence loss of
`(2 * sqrt(2) / (1 + 2) - 1) * 100`, or about -5.72%.

Fee APR annualizes the last 24 hours of stored AMM counter-asset volume, applies
the pool's fee basis points, and divides by twice the current counter-asset
reserve (the pool's estimated value in that asset). This assumes the two
reserves have equal value at the stored spot price. If the pool has no 24-hour
opening snapshot or no usable reserve value, the corresponding net figure is
`null` rather than a fabricated zero.
19 changes: 19 additions & 0 deletions openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,25 @@
"timestamp": {
"type": "string",
"format": "date-time"
},
"window": {
"type": "string",
"enum": ["24h"]
},
"feeAprPct": {
"type": "number",
"nullable": true
},
"impermanentLossPct": {
"type": "number",
"nullable": true
},
"netReturn24hPct": {
"type": "number",
"nullable": true
},
"estimate": {
"type": "boolean"
}
}
}
Expand Down
14 changes: 14 additions & 0 deletions openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,20 @@ paths:
timestamp:
type: string
format: date-time
window:
type: string
enum: [24h]
feeAprPct:
type: number
nullable: true
impermanentLossPct:
type: number
nullable: true
netReturn24hPct:
type: number
nullable: true
estimate:
type: boolean
'402':
$ref: '#/components/responses/PaymentRequired'
'429':
Expand Down
38 changes: 36 additions & 2 deletions src/api/rest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
installResponseValidation,
} from './schemas'
import { getDepth } from '../pricing/depth'
import { impermanentLossPct } from '../pricing/impermanentLoss'

function makePairKey(a: string, b: string): string {
return [a, b].sort().join('/')
Expand Down Expand Up @@ -225,18 +226,51 @@ export async function registerRESTRoutes(app: FastifyInstance) {
WHERE ids.pool_id IS NOT NULL
)
SELECT l.pool_id, l.asset_a, l.asset_b,
l.reserve_a::float, l.reserve_b::float, l.spot_price::float, l.fee_bp, l.timestamp
l.reserve_a::float, l.reserve_b::float, l.spot_price::float, l.fee_bp, l.timestamp,
opening.spot_price::float AS window_start_price,
COALESCE(activity.counter_volume, 0)::float AS window_counter_volume
FROM ids
CROSS JOIN LATERAL (
SELECT * FROM pool_snapshots s
WHERE s.network = $1 AND s.pool_id = ids.pool_id
ORDER BY s.timestamp DESC
LIMIT 1
) l
LEFT JOIN LATERAL (
SELECT s.spot_price FROM pool_snapshots s
WHERE s.network = $1 AND s.pool_id = ids.pool_id
AND s.timestamp >= NOW() - INTERVAL '24 hours'
ORDER BY s.timestamp ASC LIMIT 1
) opening ON true
LEFT JOIN LATERAL (
SELECT SUM(p.counter_volume) AS counter_volume FROM price_points p
WHERE p.network = $1 AND p.pool_id = ids.pool_id AND p.source = 'AMM'
AND p.timestamp >= NOW() - INTERVAL '24 hours'
) activity ON true
WHERE ids.pool_id IS NOT NULL`,
[network]
)
return { pools: result.rows }
const pools = result.rows.map(row => {
const startPrice = row.window_start_price == null ? null : Number(row.window_start_price)
const endPrice = Number(row.spot_price)
const divergenceLossPct = startPrice === null ? null : impermanentLossPct(startPrice, endPrice)
const reserveB = Number(row.reserve_b)
const poolValueB = Number.isFinite(reserveB) ? 2 * reserveB : 0
const feeBp = Number(row.fee_bp) || 0
const volumeB = Number(row.window_counter_volume) || 0
const feeReturn24hPct = poolValueB > 0 ? (volumeB * feeBp / 10_000 / poolValueB) * 100 : null
return {
...row,
window: '24h',
feeAprPct: feeReturn24hPct === null ? null : feeReturn24hPct * 365,
impermanentLossPct: divergenceLossPct,
netReturn24hPct: divergenceLossPct === null || feeReturn24hPct === null
? null
: feeReturn24hPct + divergenceLossPct,
estimate: true,
}
})
return { pools }
})

// GET /price/:assetA/:assetB/depth?amount=1000
Expand Down
5 changes: 5 additions & 0 deletions src/api/schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,11 @@ export const poolsResponseSchema = {
reserve_b: { type: ['number', 'null'] },
spot_price: { type: ['number', 'null'] },
fee_bp: { type: ['integer', 'null'] },
window: { type: 'string', enum: ['24h'] },
feeAprPct: { type: ['number', 'null'] },
impermanentLossPct: { type: ['number', 'null'] },
netReturn24hPct: { type: ['number', 'null'] },
estimate: { type: 'boolean' },
},
},
},
Expand Down
8 changes: 8 additions & 0 deletions src/pricing/impermanentLoss.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/** Constant-product 50/50 LP divergence loss, as a percent of initial value. */
export function impermanentLossPct(startPrice: number, endPrice: number): number | null {
if (!Number.isFinite(startPrice) || !Number.isFinite(endPrice) || startPrice <= 0 || endPrice <= 0) {
return null
}
const ratio = endPrice / startPrice
return (2 * Math.sqrt(ratio) / (1 + ratio) - 1) * 100
}
Loading