> ## 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.

# Ticks 和 positions

> CLMM 如何将价格离散化为 ticks，tick 数组如何压缩存储，以及管理 position 位置的规则。

<Info>
  **本页内容由 AI 自动翻译，所有内容以英文版本为准。**

  [查看英文版 →](/products/clmm/ticks-and-positions)
</Info>

## 为什么需要 ticks

CLMM 的流动性集中在价格范围内。为了在链上使范围易于处理，价格被量化为整数 **ticks**，其中每个 tick 是前一个的常数倍数：

$$
\text{price}(i) = 1.0001^{\,i}
$$

一个 tick 对应 0.01% 的价格变动，或约 1 个基点。映射关系如下：

| Tick 索引 `i` | 价格倍数 |
| - | - |
| `0` | `1.0000` |
| `100` | `1.0100`（≈ +1.00%） |
| `-100` | `0.9900`（≈ −0.99%） |
| `10000` | `2.7181`（≈ e） |
| `MAX_TICK = 443636` | ≈ `1.84e19` |
| `MIN_TICK = -443636` | ≈ `5.42e-20` |

`MIN_TICK` 和 `MAX_TICK` 的选择确保 `sqrt_price_x64` 在两端都能适配 `u128`。每个池都强制 `tick_lower >= MIN_TICK` 和 `tick_upper <= MAX_TICK`。实际上，Web UI 会将范围限制在更窄的范围内，以防止用户将流动性锁定在无法到达的 ticks 中。

## Tick 间距

池的 `AmmConfig` 固定了一个 **tick 间距** — position 唯一允许用作端点的 ticks。如果 `tick_spacing = 60`，只有 ticks `…, −120, −60, 0, 60, 120, …` 是有效的。尝试以端点 `31` 打开 position 会因 `TickAndSpacingNotMatch` 而回滚。

常见的已发布间距：

| 费用等级 | `trade_fee_rate` | Tick 间距 | 每个 tick position 的最粗价格步长 |
| - | - | - | - |
| 0.01% | `100` | 1 | 0.01% |
| 0.05% | `500` | 10 | 0.10% |
| 0.25% | `2500` | 60 | 0.60% |
| 1.00% | `10000` | 120 | 1.21% |

间距越粗，需要初始化的 tick 数组越少，打开宽 position 的成本越低，价格边界越模糊。波动性交易对通常在 120 间距等级中；稳定币对在 1 间距等级中。

## Tick 数组

池不会在单独的账户中存储每个 tick 的状态。相反，`TICK_ARRAY_SIZE` 个相邻的 ticks（当前 Raydium CLMM 中为 60 个）被打包到单个 `TickArrayState` 中。数组的第一个 tick 是其 `start_tick_index`，它覆盖恰好 `TICK_ARRAY_SIZE * tick_spacing` 个整数 tick 单位。

对于 `tick_spacing = 60` 和 `TICK_ARRAY_SIZE = 60`：

* 每个 tick 数组跨越 `60 × 60 = 3600` 个整数 ticks。
* `start_tick_index` 是 3600 的倍数：`…, -7200, -3600, 0, 3600, 7200, …`。

在 `tick_spacing = 60` 时，position 端点 `t = 2040` 位于 `start_tick_index = 0` 的 tick 数组中。Position 端点 `t = 4200` 位于 `start_tick_index = 3600` 的数组中。

### 何时创建数组

Tick 数组是延迟初始化的：**第一个** 引用其中任何 tick 的 position 会初始化该数组并支付租金。Swaps 不会初始化 tick 数组 — 它们使用位图跳过未初始化的数组。不存在单独的初始化 tick 数组指令：`OpenPosition*` 和 `IncreaseLiquidity*` 会在内部调用 `TickArrayState::get_or_create_tick_array`，并由 `payer` 出资分配缺失的数组。你只需传入 tick 数组的 PDA —— 即使它仍未初始化、仍归系统程序所有。

### Tick 数组不会被关闭

一旦 tick 数组被初始化，它就会在池的整个生命周期内持续存在。程序**不**提供关闭 tick 数组的路径，即使 `initialized_tick_count` 返回到零也不行。Tick 数组没有租金恢复；第一个接触数组的 position 支付的租金会永久锁定在该账户中。这是一个有意的权衡：重用现有 tick 数组对后续每个 position 都是免费的，所以一个交易频繁的池只需为每个 `(pool, start_tick_index)` 槽支付一次租金，无论流动性变化如何。

### 位图

找到"当前 tick 左/右的下一个已初始化 tick"必须很快 — swap 可能会跨越许多 ticks。池在 `PoolState` 中内联存储一个每 tick 数组 1 位的位图，覆盖 tick 0 周围 **±512** 个数组的范围（`TICK_ARRAY_BITMAP_SIZE = 512`；该字段是 `[u64; 16]` = 1,024 位，按 512 个负索引 / 512 个正索引平分，即数组索引 −512 … +511）。在该范围之外（全范围 positions、特殊设置），`TickArrayBitmapExtension` 提供溢出。

Swap 遍历位图：`lowest_set_bit_above(tick_current_array_index)` 给出 swap 正在跨越的一侧的下一个具有已初始化 tick 的数组。在该数组内，类似的位扫描定位下一个已初始化的 tick。

## `liquidity_gross` 和 `liquidity_net`

每个**已初始化**的 tick 存储两个流动性值：

* **`liquidity_gross`** — 所有将此 tick 作为端点引用的 positions 的 `L` 之和。当 `liquidity_gross` 达到零时，tick 变为未初始化，可以从位图中移除。
* **`liquidity_net`** — 当价格**向上**跨越此 tick（在 tick 空间中从左到右）时，对池级 `liquidity` 的*有符号*变化。如果此 tick 是大小为 `L` 的 position 的下界，它贡献 `+L`；如果它是该 position 的上界，它贡献 `−L`。

工作示例：同一池上的两个 positions。

* Position A：`tick_lower = -120`，`tick_upper = 0`，流动性 `L_A = 100`。
* Position B：`tick_lower = -60`，`tick_upper = 60`，流动性 `L_B = 50`。

逐 tick 状态：

| Tick | 被触及者 | `liquidity_gross` | `liquidity_net` |
| - | - | - | - |
| `-120` | A 下界 | 100 | +100 |
| `-60` | B 下界 | 50 | +50 |
| `0` | A 上界 | 100 | −100 |
| `60` | B 上界 | 50 | −50 |

不同 `tick_current` 值的池级 `liquidity`：

* `tick_current = -180`：`liquidity = 0`（任何 position 之前）
* `tick_current = -90`：`liquidity = 100`（仅在 A 内）
* `tick_current = -30`：`liquidity = 150`（在 A 和 B 内）
* `tick_current = 30`：`liquidity = 50`（仅在 B 内）
* `tick_current = 90`：`liquidity = 0`（超过两者）

在 swap 期间的每次 tick 跨越时，程序将 `liquidity_net`（可能为负）添加到 `PoolState.liquidity`。这正是 Uniswap v3 的机制。

## Positions 作为 NFTs

Raydium CLMM position 是一个 NFT。打开 position 会将一个全新的 mint（供应量为 1）铸造到调用者的钱包中，该 mint 的权限由 CLMM 程序拥有。程序将 position 所有权关联到**在 CPI 时在该 mint 的 ATA 中持有余额的任何人**。

后果：

* **Positions 通常是可转移的。** 钱包可以通过转移 NFT 来出售或空投 position。新持有者随后可以调用 `CollectRewards`、`IncreaseLiquidity` 等。例外是在受限发行者路径下冻结的 position。
* **Positions 在 CLMM 外可寻址。** 市场和钱包像显示其他 NFTs 一样显示 positions。SDK 在 mint 元数据上设置合理的 `name`/`symbol`。
* **Position 的 PDA 从 NFT mint 派生。** 你可以找到 `PersonalPositionState` 而无需知道当前持有者是谁。

### 受限发行者 positions

2026-08 升级后创建的每个 position NFT mint 都将其 CLMM `pool_state` 记录为冻结权限。**这并不意味着每个新 position 都被冻结。** 对于普通池和每个非匹配 position，NFT 代币账户保持未冻结和可转移。池 PDA 无法在 CLMM 程序外签名，CLMM 不提供通用冻结指令。

冻结需要满足以下两个条件：

1. Position 通过 `OpenPositionV2` 或 `OpenPositionWithToken22Nft` 打开。
2. 至少一个池金库 mint 从程序的硬编码受限发行者列表中携带冻结权限。

只有当两个条件都满足时，CLMM 才会在铸造后立即冻结新创建的 position NFT 账户。`OpenPosition` V1 不应用此过滤器。参见 [`reference/program-addresses`](/zh/reference/program-addresses#clmm-restricted-issuer-freeze-authorities) 了解当前列表。

冻结的 position：

* 无法将其 NFT 转移到另一个代币账户。
* 无法更改 NFT 代币账户的所有者。
* 当记录的所有者签名时，仍可以增加或减少流动性并收集费用或奖励。
* 仍可以关闭。`ClosePosition` 使用池 PDA 解冻 NFT 账户，然后在同一指令中销毁 NFT 并关闭 position 账户。

现有 positions 不会被迁移或追溯冻结。升级前创建的 Position NFT mints 保留其之前的冻结权限设置。

<Warning>
  关闭冻结 position 的客户端必须将 position 的 `pool_state` 作为 `ClosePosition` 的第一个剩余账户附加。声明的 IDL 账户列表保持不变，因此较旧的客户端可以成功打开冻结 position，但稍后可能因 `AccountLack` 而无法关闭它。在支持这些池之前更新关闭构建器。
</Warning>

### Token-2022 positions

CLMM 可以通过 `OpenPositionV2` 在经典 SPL Token 下铸造 position NFT，或通过 `OpenPositionWithToken22Nft` 在 Token-2022 下铸造。两个 V2 路径都检查池金库 mints 并应用相同的受限发行者冻结规则。`OpenPosition` V1 是遗留的经典代币路径，无法服务具有 Token-2022 金库 mints 的池。钱包和市场兼容性不同；Raydium 的 UI 跟踪两个 NFT 程序。

## 允许范围规则

在 `OpenPosition` 时，程序强制执行：

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` 的 tick 数组 PDA。它们**不必**已初始化 —— 程序会自己创建缺失的那一个。
5. 位图扩展账户，如果此 position 扩展到扩展范围。

如果任何检查失败，指令会回滚：`TickInvalidOrder`（规则 1）、`TickAndSpacingNotMatch`（规则 2）、`InvalidTickIndex`（规则 3）或 `MissingTickArrayBitmapExtensionAccount`（规则 5）；`NotApproved` 对应另一种情况，即 `pool_state.status` 设置了开仓位。参见 [`reference/error-codes`](/zh/reference/error-codes)。

## "范围内"vs"范围外"

当 `tick_lower <= tick_current < tick_upper` 时，position 是**范围内**的。只有范围内的 positions 对 `PoolState.liquidity` 有贡献，因此只有它们赚取 swap 费用。

范围外的 position：

* 持有**一个**代币的 100%。如果 `tick_current < tick_lower` —— 价格在范围**下方** —— position 完全是 **token0**；如果 `tick_current >= tick_upper` —— 价格在范围**上方** —— 则完全是 **token1**。价格以 `token1/token0` 计价，因此被上涨价格走过的 position 最终持有报价资产。
* **不**赚取 swap 费用。
* **确实**继续累积奖励，如果池的奖励流向范围外流动性发出 — 但 Raydium 的默认行为是"仅向范围内发出"，与 Uniswap v3 约定相匹配。参见 [`products/clmm/fees`](/zh/products/clmm/fees)。

管理 CLMM positions 的 LPs 花费大部分精力将 positions 保持在范围内，因为价格会移动。

## 常见集成陷阱

* **非间距端点。** 从目标价格计算 tick 的代码必须在将其传递给 `OpenPosition` 之前对齐到 `tick_spacing` 的倍数，否则会因 `TickAndSpacingNotMatch` 回滚。SDK 助手是 `TickUtil.getPriceAndTick({ price, mintADecimals, mintBDecimals, zeroForOne, tickSpacing })`（注意：`TickUtil`，单数形式）；自制数学通常会跳过对齐。
* **缺失 tick 数组。** 打开宽 position 会接触两个 tick 数组；忘记将它们作为可写账户传递会导致回滚。它们不需要事先存在 —— 程序会创建缺失的那一个 —— 但该*账户*必须在列表中。
* **Swap 后的陈旧 tick。** `tick_current` 可以在一次 swap 中跨越许多 ticks。如果你的 UX 从一个 RPC 调用显示"当前 tick"，然后在稍后的调用中打开 position，相对于实时价格的相对位置可能相差数十个 ticks。在签名前重新获取。
* **带有额外元数据的 Position NFTs。** 如果你构建一个识别 Raydium positions 的钱包，使用 position PDA / 程序数据，而不是硬编码的元数据字段。新 position mints 在创建时使用池 PDA 作为 mint 和冻结权限；在铸造单个 NFT 后移除 mint 权限。
* **假设每个 position 都是可转移的。** 在显示转移、市场、托管或销毁和赚取操作之前，读取 NFT 代币账户的 `isFrozen` 状态。

## 接下来去哪里

* [Math](/zh/products/clmm/math) — swap 步进和费用增长推导，tick 边界参与其中。
* [Accounts](/zh/products/clmm/accounts) — `TickArrayState` 和 `PositionState` 布局。
* [Fees and rewards](/zh/products/clmm/fees) — 范围内性如何控制费用累积。
* [`algorithms/clmm-math`](/zh/algorithms/clmm-math) — 集中流动性公式的共享推导。

来源：

* [`raydium-io/raydium-clmm` — `tick_array`、`tick`、`position` 模块](https://github.com/raydium-io/raydium-clmm)
* "Uniswap v3 Core" 白皮书，§6（ticks），§7（费用增长）


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.