Skip to main content
Эта страница переведена с помощью ИИ. За эталон принимается английская версия.Открыть английскую версию →

Зачем нужны тики

Ликвидность CLMM сосредоточена в диапазонах цен. Чтобы сделать диапазоны управляемыми в цепи, цены квантуются в целые числа тики, где каждый тик — это постоянный множитель предыдущего: price(i)=1.0001i\text{price}(i) = 1.0001^{\,i} Один тик соответствует движению цены на 0,01%, или примерно 1 базисный пункт. Соответствие: MIN_TICK и MAX_TICK выбраны так, чтобы sqrt_price_x64 помещался в u128 на обоих концах. Каждый пул гарантирует, что tick_lower >= MIN_TICK и tick_upper <= MAX_TICK. На практике веб-интерфейс ограничивает диапазон чем-то намного более узким, чтобы предотвратить блокировку ликвидности в недостижимых тиках.

Интервал тиков

AmmConfig пула фиксирует интервал тиков — единственные тики, которые позиция может использовать в качестве конечных точек. Если tick_spacing = 60, допустимы только тики …, −120, −60, 0, 60, 120, …. Попытка открыть позицию с конечной точкой 31 вернёт ошибку InvalidTickIndex. Распространённые опубликованные интервалы: Чем грубее интервал, тем меньше массивов тиков нужно инициализировать, тем дешевле открыть широкую позицию и тем менее чёткая граница цены. Волатильные пары обычно находятся в уровнях с интервалом 120; стейблкойны — в уровнях с интервалом 1.

Массивы тиков

Пул не хранит состояние для каждого тика в отдельных аккаунтах. Вместо этого TICK_ARRAY_SIZE соседних тиков (60 в текущем Raydium CLMM) упакованы в один TickArrayState. Первый тик массива — это его start_tick_index, и он охватывает ровно TICK_ARRAY_SIZE * tick_spacing целых единиц тиков. Для tick_spacing = 60 и TICK_ARRAY_SIZE = 60:
  • Каждый массив тиков охватывает 60 × 60 = 3600 целых тиков.
  • start_tick_index — это кратное 3600: …, -7200, -3600, 0, 3600, 7200, ….
Конечная точка позиции t = 2040 при tick_spacing = 60 находится в массиве тиков с start_tick_index = 0. Конечная точка позиции t = 4200 находится в массиве с start_tick_index = 3600.

Когда создаётся массив

Массив тиков ленивый: первая позиция, которая ссылается на любой тик внутри него, инициализирует массив, оплачивая ренту. Свопы не инициализируют массивы тиков — они пропускают неинициализированные массивы, используя битовую карту. Поток открытия позиции SDK проверяет выбранный диапазон, вычисляет список массивов тиков, которые он затрагивает, и добавляет инструкции init_tick_array в ту же транзакцию, что и OpenPosition, если какие-то отсутствуют.

Массивы тиков не закрываются

После инициализации массива тиков он сохраняется на протяжении всей жизни пула. Программа не предоставляет способ закрыть массив тиков, даже после того, как initialized_tick_count вернётся к нулю. Нет возврата ренты для массивов тиков; рента, оплаченная первой позицией, которая коснулась массива, навсегда заблокирована в этом аккаунте. Это намеренный компромисс: повторное использование существующего массива тиков бесплатно для каждой последующей позиции, поэтому активно торгуемый пул платит стоимость ренты только один раз за слот (pool, start_tick_index) независимо от оборота.

Битовая карта

Поиск «следующего инициализированного тика слева/справа от текущего тика» должен быть быстрым — свопы могут пересекать много тиков. Пул хранит битовую карту с одним битом на массив тиков встроенной в PoolState для диапазона ±1024 массивов вокруг тика 0. За пределами этого диапазона (позиции полного диапазона, экзотические установки) TickArrayBitmapExtension предоставляет переполнение. Свопы проходят по битовой карте: lowest_set_bit_above(tick_current_array_index) даёт следующий массив с инициализированным тиком на стороне, в которую движется свопа. Внутри этого массива аналогичное сканирование битов находит следующий инициализированный тик.

liquidity_gross и liquidity_net

Каждый инициализированный тик хранит два значения ликвидности:
  • liquidity_gross — сумма L по всем позициям, которые ссылаются на этот тик как на конечную точку. Когда liquidity_gross достигает нуля, тик становится неинициализированным и может быть удалён из битовой карты.
  • liquidity_netподписанное изменение ликвидности уровня пула при пересечении этого тика в восходящем направлении (слева направо в пространстве тиков). Если этот тик — нижняя граница позиции размером L, он вносит +L; если это верхняя граница этой позиции, он вносит −L.
Рабочий пример: две позиции в одном пуле.
  • Позиция A: tick_lower = -120, tick_upper = 0, ликвидность L_A = 100.
  • Позиция B: tick_lower = -60, tick_upper = 60, ликвидность L_B = 50.
Состояние по тикам: Ликвидность уровня пула для разных значений tick_current:
  • tick_current = -180: liquidity = 0 (перед любой позицией)
  • tick_current = -90: liquidity = 100 (только внутри A)
  • tick_current = -30: liquidity = 150 (внутри A и B)
  • tick_current = 30: liquidity = 50 (только внутри B)
  • tick_current = 90: liquidity = 0 (после обеих)
При каждом пересечении тика во время свопа программа добавляет liquidity_net (возможно, отрицательное) к PoolState.liquidity. Это точный механизм Uniswap v3.

Позиции как NFT

Позиция Raydium CLMM — это NFT. Открытие позиции создаёт совершенно новый минт с предложением 1 в кошельке вызывающей стороны, и полномочия минта принадлежат программе CLMM. Программа привязывает владение позицией к тому, кто держит баланс в ATA этого минта во время CPI. Последствия:
  • Позиции обычно передаваемы. Кошелёк может продать или раздать позицию, передав NFT. Новый владелец может затем вызвать CollectRewards, IncreaseLiquidity и т. д. Исключение — позиция, заморозенная по пути ограниченного издателя ниже.
  • Позиции адресуемы вне CLMM. Маркетплейсы и кошельки отображают позиции как другие NFT. SDK устанавливает разумные name/symbol в метаданных минта.
  • PDA позиции получена из минта NFT. Вы можете найти PersonalPositionState без знания того, кто его в данный момент держит.

Позиции ограниченного издателя

Каждый минт NFT позиции, созданный после обновления 2026-08, записывает свой CLMM pool_state как полномочие заморозки. Это не означает, что каждая новая позиция заморожена. Для обычных пулов и каждой несовпадающей позиции аккаунт токена NFT остаётся разморозенным и передаваемым. PDA пула не может подписывать вне программы CLMM, и CLMM не предоставляет инструкцию общего назначения для заморозки. Заморозка требует обоих этих условий:
  1. Позиция открыта через OpenPositionV2 или OpenPositionWithToken22Nft.
  2. По крайней мере один минт хранилища пула имеет полномочие заморозки из списка ограниченных издателей, жёстко закодированного в программе.
Только когда оба условия выполнены, CLMM замораживает вновь созданный аккаунт NFT позиции сразу после создания. OpenPosition V1 не применяет этот фильтр. См. reference/program-addresses для текущего списка. Замороженная позиция:
  • Не может передать свой NFT на другой аккаунт токена.
  • Не может изменить владельца аккаунта токена NFT.
  • Может по-прежнему увеличивать или уменьшать ликвидность и собирать комиссии или награды, когда подписывает записанный владелец.
  • Может по-прежнему закрываться. ClosePosition использует PDA пула для разморозки аккаунта NFT, затем сжигает NFT и закрывает аккаунты позиции в одной инструкции.
Существующие позиции не переносятся и не замораживаются ретроактивно. Минты NFT позиций, созданные до обновления, сохраняют свои предыдущие параметры полномочий заморозки.
Клиент, закрывающий замороженную позицию, должен добавить pool_state позиции в качестве первого оставшегося аккаунта к ClosePosition. Объявленный список аккаунтов IDL не изменяется, поэтому старые клиенты могут успешно открыть замороженную позицию, но позже не смогут закрыть её с ошибкой AccountLack. Обновите построитель закрытия перед поддержкой этих пулов.

Позиции Token-2022

CLMM может создать NFT позиции под классическим SPL Token через OpenPositionV2 или под Token-2022 через OpenPositionWithToken22Nft. Оба пути V2 проверяют минты хранилища пула и применяют то же правило заморозки ограниченного издателя. OpenPosition V1 — это устаревший путь классического токена и не может обслуживать пул с минтами хранилища Token-2022. Совместимость кошельков и маркетплейсов различается; пользовательский интерфейс Raydium отслеживает обе программы NFT.

Правила допустимого диапазона

При открытии позиции программа гарантирует:
  1. tick_lower < tick_upper.
  2. tick_lower % tick_spacing == 0 и tick_upper % tick_spacing == 0.
  3. MIN_TICK <= tick_lower и tick_upper <= MAX_TICK.
  4. Вызывающая сторона предоставила массивы тиков, содержащие tick_lower и tick_upper — либо уже инициализированные, либо через init_tick_array в той же транзакции.
  5. Аккаунт расширения битовой карты, если эта позиция расширяется в диапазон расширения.
Если какая-либо проверка не пройдёт, инструкция вернёт ошибку InvalidTickIndex, NotApproved или InsufficientLiquidity в зависимости от того, какое ограничение. См. reference/error-codes.

«В диапазоне» против «вне диапазона»

Позиция в диапазоне, когда tick_lower <= tick_current < tick_upper. Только позиции в диапазоне вносят вклад в PoolState.liquidity и поэтому только они получают комиссии за свопы. Позиция вне диапазона:
  • Держит 100% одного токена (того, мимо которого прошёл её диапазон). В частности, если tick_current < tick_lower, позиция держит только token1 (она уже была «продана» движением цены); если tick_current >= tick_upper, она держит только token0.
  • Не получает комиссии за свопы.
  • Продолжает накапливать награды, если потоки вознаграждений пула выдают вознаграждения вне диапазона ликвидности — но поведение Raydium по умолчанию — «выдавать только в диапазоне», соответствуя соглашению Uniswap v3. См. products/clmm/fees.
LP, управляющие позициями CLMM, тратят большую часть своего внимания на то, чтобы держать позиции в диапазоне по мере движения цены.

Распространённые ошибки интеграции

  • Конечные точки вне интервала. Код, который вычисляет тик из целевой цены, должен привязать его к кратному tick_spacing перед передачей в OpenPosition. Помощники SDK (TickUtils.getTickWithPriceAndTickspacing) делают это; самодельная математика часто нет.
  • Отсутствующие массивы тиков. Открытие широкой позиции может потребовать инициализации нескольких массивов тиков; забывание передать их как записываемые аккаунты вернёт ошибку. SDK openPositionFromBase возвращает список для вас.
  • Устаревший тик после свопа. tick_current может пересечь много тиков в одном свопе. Если ваш UX показывает «текущий тик» из одного вызова RPC, а затем открывает позицию в более позднем, относительная позиция к живой цене может быть смещена на десятки тиков. Повторно получите данные прямо перед подписанием.
  • NFT позиций с дополнительными метаданными. Если вы создаёте кошелёк, который распознаёт позиции Raydium, используйте PDA позиции / данные программы, а не жёстко закодированное поле метаданных. Новые минты позиций используют PDA пула как полномочия минта и заморозки при создании; полномочия минта удаляются после создания единственного NFT.
  • Предположение, что каждая позиция передаваема. Прочитайте состояние isFrozen аккаунта токена NFT перед отображением действий передачи, маркетплейса, условного депозита или Burn & Earn.

Что дальше

  • Математика — пошаговое объяснение свопа и вывод роста комиссий, в котором участвуют границы тиков.
  • Аккаунты — макеты TickArrayState и PositionState.
  • Комиссии и награды — как нахождение в диапазоне управляет накоплением комиссий.
  • algorithms/clmm-math — общий вывод формул сосредоточенной ликвидности.
Источники: