> ## Documentation Index
> Fetch the complete documentation index at: https://docs.raydium.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Математика сосредоточенной ликвидности

> Представление sqrt-price, формулы преобразования ликвидности в объёмы, шаги свопа внутри и между тиками, учёт комиссий — математика за CLMM Raydium.

<Info>
  **Эта страница переведена с помощью ИИ. За эталон принимается английская версия.**

  [Открыть английскую версию →](/algorithms/clmm-math)
</Info>

## Почему sqrt-price, а не price

CLMM семейства Uniswap-v3 представляют цену как её квадратный корень, хранящийся в виде фиксированной точки `Q64.64`:

```
sqrt_price_x64 = floor(sqrt(price) · 2^64)
```

Три причины:

1. **Линейная математика ликвидности.** Объём token0 или token1 в диапазоне цен оказывается линейной функцией `sqrt_price`, а не `price`. Хранение `sqrt_price` позволяет шагу свопа вычислять эти линейные формулы без вычисления квадратного корня.
2. **Контроль переполнения.** Произведение `sqrt_price · L` умещается в `u256` при всех разумных параметрах; `price · L` может переполниться намного раньше.
3. **Равномерная математика тиков.** Поскольку тики определены как `1.0001^i`, то `sqrt(price) = 1.00005^i` также является точной степенью лестницы 1.00005. Каждое пересечение тика переводится в небольшое умножение в пространстве `sqrt_price_x64`.

Price и sqrt-price находятся в взаимно однозначном соответствии; преобразование: `price = (sqrt_price_x64 / 2^64)^2`.

## Решётка тиков

Цены дискретизируются на сетку:

```
price(tick_i) = 1.0001^i
```

`tick_i` — это `i32`. Активный диапазон — `[MIN_TICK, MAX_TICK] = [−443636, 443636]`, что даёт диапазон цен примерно `[2^−128, 2^128]`. Значение `tick_spacing` каждого пула устанавливается его уровнем комиссии: меньшие значения для узких пар (например, уровень 0.01% для стейблкойнов использует spacing 1), большие значения для волатильных пар (уровень 0.25% использует 60, уровень 1% использует 120).

Позиции должны иметь `tick_lower` и `tick_upper`, выравненные по `tick_spacing`. Активные тики пула (те, у которых начинается или заканчивается ликвидность) — единственные тики, которые интересуют шаг свопа.

## Преобразование ликвидности в объёмы

Для позиции с ликвидностью `L` и диапазоном цен `[sqrt_lo, sqrt_hi]` (все значения в `sqrt_price`):

| Состояние пула                           | Объём token0                                    | Объём token1              |
| ---------------------------------------- | ----------------------------------------------- | ------------------------- |
| Цена выше диапазона (`sqrt_p ≥ sqrt_hi`) | 0                                               | `L · (sqrt_hi − sqrt_lo)` |
| Цена внутри диапазона                    | `L · (sqrt_hi − sqrt_p) / (sqrt_p · sqrt_hi)`   | `L · (sqrt_p − sqrt_lo)`  |
| Цена ниже диапазона (`sqrt_p ≤ sqrt_lo`) | `L · (sqrt_hi − sqrt_lo) / (sqrt_lo · sqrt_hi)` | 0                         |

Выведение: дифференцируем инвариант CPMM локально. Внутри любого одного диапазона тика позиция ведёт себя как CPMM с виртуальными резервами `(x_v, y_v)`, выбранными так, чтобы текущие `(sqrt_p, L)` пула были согласованы с `L = sqrt(x_v · y_v)`. Интегрирование от `sqrt_p` до границы диапазона даёт указанные выше объёмы.

**Обратные формулы** (используются при создании позиции с заданным `amount0` или `amount1`):

```
L_from_amount0(amount0, sqrt_lo, sqrt_hi, sqrt_p) =
    amount0 · sqrt_p · sqrt_hi / (sqrt_hi − sqrt_p)

L_from_amount1(amount1, sqrt_lo, sqrt_hi, sqrt_p) =
    amount1 / (sqrt_p − sqrt_lo)

// Для симметричного взноса в позицию внутри диапазона берём минимум.
L = min(L_from_amount0, L_from_amount1)
```

## Шаг свопа внутри одного тика

Внутри одного диапазона тика пул ведёт себя как CPMM. При текущем `sqrt_p` и целевом `sqrt_target`:

```
Δamount0_step = L · (sqrt_target − sqrt_p) / (sqrt_p · sqrt_target)     // при свопе на token0
Δamount1_step = L · (sqrt_target − sqrt_p)                              // при свопе на token1
```

### Шаг с точным входом

При заданном `Δin_remaining`:

```
// Кандидат на новый sqrt_p если мы заполним до границы тика:
sqrt_after_full = sqrt_target
amount_to_full  = Δamount_in_to_reach(sqrt_p → sqrt_target)

if Δin_remaining ≥ amount_to_full:
    // потребляем остаток бакета
    sqrt_p'         = sqrt_target
    Δin_consumed    = amount_to_full
    Δout            = amount_out_at_boundary
else:
    // не пересекаем; решаем для финального sqrt_p
    sqrt_p'         = L · sqrt_p / (L + Δin_remaining · sqrt_p)      // для свопов 0→1
    Δin_consumed    = Δin_remaining
    Δout            = L · (sqrt_p − sqrt_p')                          // пропорционально Δsqrt
```

Своп `0→1` снижает `sqrt_p` (цена падает по мере продажи token0). Своп `1→0` повышает её. Формулы симметричны с перестановкой `sqrt_p` и `sqrt_target`.

### Шаг с точным выходом

Та же структура, но решаем для `Δin` вместо `Δout`.

## Цикл свопа между несколькими тиками

Своп итерирует по тикам до истощения входа или достижения лимита цены:

```
while Δin_remaining > 0 and sqrt_p != sqrt_price_limit:
    next_tick = find_next_initialized_tick(pool.tick_current, direction)
    sqrt_target = min(next_tick.sqrt_price, sqrt_price_limit)       // в направлении

    (Δin, Δout, sqrt_p') = single_step(sqrt_p, sqrt_target, L, Δin_remaining)

    Δin_remaining -= Δin
    accumulated_out += Δout

    if sqrt_p' == next_tick.sqrt_price:
        // пересечение тика
        L += next_tick.liquidity_net * direction_sign
        flip_fee_growth_outside(next_tick)
        match_limit_orders_at_tick(next_tick, ...)        // см. products/clmm/math
        pool.tick_current = next_tick.tick_index
    sqrt_p = sqrt_p'
```

Каждый `single_step` использует текущий `L` пула. `L` изменяется **только** при пересечении инициализированного тика. Ликвидность между тиками постоянна, что позволяет получить замкнутую формулу для шага.

`liquidity_net` в тике — это знаковая сумма ликвидностей позиций, начинающихся в этом тике, минус те, что заканчиваются там. При пересечении вверх добавляем `liquidity_net`; при пересечении вниз вычитаем.

Когда в пуле открыты лимитные ордера на тике, шаг пересечения тика также оппортунистически потребляет часть входного объёма свопа для заполнения этих ордеров (FIFO по группам). Алгоритм сопоставления и динамическая комиссия, которая может быть применена поверх базового шага, документированы в [`products/clmm/math`](/ru/products/clmm/math); они не изменяют замкнутые формулы одного шага выше.

## Аккумуляторы роста комиссий

CLMM отслеживает комиссии на единицу активной ликвидности, за каждую сторону, глобально и по тикам:

```
fee_growth_global_0_x64     // Q64.64, монотонно возрастающий
fee_growth_global_1_x64
tick.fee_growth_outside_0_x64   // "комиссии, начисленные, пока этот тик был вне активного диапазона"
tick.fee_growth_outside_1_x64
```

На каждом `single_step`:

```
step_lp_fee = (Δin · fee_rate) · (1 − protocol_fraction − fund_fraction)
fee_growth_global += step_lp_fee · 2^64 / L     // только для входной стороны
```

(Значение `fee_growth_global` для другой стороны не меняется на этом шаге, так как никакой токен с той стороны не был выплачен как входной.)

При пересечении тика программа **меняет** `fee_growth_outside`:

```
tick.fee_growth_outside_0_x64 = fee_growth_global_0_x64 − tick.fee_growth_outside_0_x64
tick.fee_growth_outside_1_x64 = fee_growth_global_1_x64 − tick.fee_growth_outside_1_x64
```

"Outside" (вне диапазона) определяется относительно `tick_current`. Когда `tick_current` находится выше тика, outside означает "ниже". Когда `tick_current` ниже, outside означает "выше". Смена меняет интерпретацию.

### `fee_growth_inside` для позиции

При заданной позиции `[tick_lower, tick_upper]` и текущем `tick_current`:

```
if tick_current >= tick_upper:
    inside = tick_lower.fee_growth_outside − tick_upper.fee_growth_outside
else if tick_current < tick_lower:
    inside = tick_upper.fee_growth_outside − tick_lower.fee_growth_outside
else:     // позиция внутри диапазона
    inside = fee_growth_global
           − tick_lower.fee_growth_outside
           − tick_upper.fee_growth_outside
```

Невыплаченные комиссии позиции для стороны токена `s`:

```
tokens_owed_s += L · (fee_growth_inside_s − fee_growth_inside_last_s) / 2^64
fee_growth_inside_last_s = fee_growth_inside_s
```

Это обновление выполняется при каждом взаимодействии с позицией (`IncreaseLiquidity`, `DecreaseLiquidity`, `CollectFees`).

## Пример с расчётами — пересечение одного тика

Пул (упрощённо):

* `sqrt_p_x64 = 2^64 · 1.0 = 2^64` (цена = 1.0)
* `L = 1_000_000`
* `tick_current = 0`
* Следующий инициализированный тик ниже: `tick = −60`, `sqrt_price = 1.0001^(−30) ≈ 0.99700`, `liquidity_net = −400_000` (этот тик заканчивает позицию, поэтому нисходящее пересечение удаляет 400k)
* Процент комиссии: 0.25%

Своп: `Δin = 10_000` token0, направление = 0→1.

**Шаг 1 — до `sqrt_target = 0.99700 · 2^64`**:

```
amount_in_to_target = L · (1/sqrt_target − 1/sqrt_p)
                    = 1_000_000 · (1/0.99700 − 1/1.0)
                    ≈ 1_000_000 · 0.003009
                    ≈ 3_009
```

3 009 \< 10 000, поэтому заполняем этот шаг полностью:

```
Δin_step  = 3_009 / (1 − 0.0025)  = 3_017    // с учётом комиссии
Δout_step = L · (sqrt_p − sqrt_target) ≈ 1_000_000 · 0.00299 ≈ 2_990
sqrt_p    = 0.99700 · 2^64
tick_current = −60
L         = 1_000_000 + (−400_000)  = 600_000         // пересекли тик
fee_growth_outside на тике −60 обновляется
Δin_remaining = 10_000 − 3_017 = 6_983
```

**Шаг 2 — с новым `L = 600_000`**:

Следующий инициализированный тик (скажем, `tick = −120`) находится на `sqrt = 0.99402`. Пересчитываем `amount_in_to_target`:

```
amount_in_to_target = 600_000 · (1/0.99402 − 1/0.99700)
                    ≈ 600_000 · 0.003010
                    ≈ 1_806
```

Всё ещё меньше `Δin_remaining`. Пересекаем снова. Продолжаем до тех пор, пока `Δin_remaining` не достигнет нуля.

Полная последовательность `Δout` накапливается в финальный выход свопа.

## Инициализация и охрана от переполнения

* `MIN_SQRT_PRICE_X64` и `MAX_SQRT_PRICE_X64` соответствуют `tick = ±443636`. Любой своп, который попытается вытолкнуть `sqrt_p` за пределы этого диапазона, будет отменён.
* Параметр `sqrt_price_limit` пользователя должен находиться в том же интервале; программа проверяет это.
* Произведения `L · Δsqrt` вычисляются в `u256`, а затем сдвигаются обратно в `u128` для предотвращения переполнения.

## Различия с Uniswap v3

* **Oracle.** `ObservationState` Raydium хранит буфер `(block_timestamp, tick_cumulative, seconds_per_liquidity_cumulative)` в виде кольца; немного другой формат провода, чем у Uniswap, но такая же математика TWAP.
* **Token-2022.** CLMM Raydium поддерживает мелинты Token-2022; вариант с комиссией за передачу требует дополнительных корректировок объёмов до и после свопа. См. [`algorithms/token-2022-transfer-fees`](/ru/algorithms/token-2022-transfer-fees).
* **Bitmap тиков.** Raydium упаковывает bitmap инициализированных тиков в `[u64; 16]` на пул для быстрого `find_next_initialized_tick`; Uniswap использует on-chain маппинг per-word. Компромисс между размером хранилища и стоимостью поиска.
* **Слоты вознаграждений.** Raydium поддерживает 3 потока вознаграждений на пул с отдельными счётчиками `reward_growth_global_x64`; такая же структура, как у аккумулятора роста комиссий.

## Указатели

* [`products/clmm/math`](/ru/products/clmm/math) — on-chain реализация и пример с расчётами с фактическими полями структуры CLMM.
* [`products/clmm/ticks-and-positions`](/ru/products/clmm/ticks-and-positions) — решётка тиков, семантика `liquidity_net`/`gross`, активного диапазона.
* [`products/clmm/fees`](/ru/products/clmm/fees) — аккумулятор роста комиссий в действии.

Источники:

* Whitepaper Uniswap v3 (каноническое выведение математики sqrt-price).
* Исходный код программы Raydium CLMM.
