# 开启 / 关闭仓位

使用自定义价格区间开仓以提供集中流动性，或关闭空仓位以收回租金。

***

### CLMM 仓位的工作原理

每个 CLMM 仓位都由你钱包中的一个 NFT 表示。一个仓位定义了：

* 一个 **价格区间** （lower tick 和 upper tick），你的流动性在该区间内处于活跃状态。
* 所 **存入的 token A 和 token B 数量** 在该区间内。

只有当池子的当前价格位于你仓位的区间内时，流动性才会赚取交易手续费。

***

### 获取池信息

开仓和关仓操作都需要池信息。

```typescript
import { ApiV3PoolInfoConcentratedItem, ClmmKeys, CLMM_PROGRAM_ID, DEVNET_PROGRAM_ID } from '@raydium-io/raydium-sdk-v2'
import { initSdk, txVersion } from '../config'

const VALID_PROGRAM_ID = new Set([
  CLMM_PROGRAM_ID.toBase58(),
  DEVNET_PROGRAM_ID.CLMM_PROGRAM_ID.toBase58(),
])
const isValidClmm = (id: string) => VALID_PROGRAM_ID.has(id)

let poolInfo: ApiV3PoolInfoConcentratedItem
let poolKeys: ClmmKeys | undefined

const raydium = await initSdk()
const poolId = 'YOUR_POOL_ID'

if (raydium.cluster === 'mainnet') {
  const data = await raydium.api.fetchPoolById({ ids: poolId })
  poolInfo = data[0] as ApiV3PoolInfoConcentratedItem
  if (!isValidClmm(poolInfo.programId)) throw new Error('target pool is not CLMM pool')
} else {
  const data = await raydium.clmm.getPoolInfoFromRpc(poolId)
  poolInfo = data.poolInfo
  poolKeys = data.poolKeys
}
```

***

### 开仓

使用 `raydium.clmm.openPositionFromBase()` 来开一个新仓位。你需要指定一个 token 数量和一个价格区间，SDK 会计算所需的另一个 token 数量。

```typescript
import {
  ApiV3PoolInfoConcentratedItem,
  TickUtils,
  PoolUtils,
  ClmmKeys,
} from '@raydium-io/raydium-sdk-v2'
import BN from 'bn.js'
import Decimal from 'decimal.js'
import { initSdk, txVersion } from '../config'
import { isValidClmm } from './utils'

const createPosition = async () => {
  const raydium = await initSdk()

  let poolInfo: ApiV3PoolInfoConcentratedItem
  const poolId = '61R1ndXxvsWXXkWSyNkCxnzwd3zUNB8Q2ibmkiLPC8ht'
  let poolKeys: ClmmKeys | undefined

  if (raydium.cluster === 'mainnet') {
    const data = await raydium.api.fetchPoolById({ ids: poolId })
    poolInfo = data[0] as ApiV3PoolInfoConcentratedItem
    if (!isValidClmm(poolInfo.programId)) throw new Error('target pool is not CLMM pool')
  } else {
    const data = await raydium.clmm.getPoolInfoFromRpc(poolId)
    poolInfo = data.poolInfo
    poolKeys = data.poolKeys
  }

  const inputAmount = 0.000001
  const [startPrice, endPrice] = [0.000001, 100000]

  const { tick: lowerTick } = TickUtils.getPriceAndTick({
    poolInfo,
    price: new Decimal(startPrice),
    baseIn: true,
  })

  const { tick: upperTick } = TickUtils.getPriceAndTick({
    poolInfo,
    price: new Decimal(endPrice),
    baseIn: true,
  })

  const epochInfo = await raydium.fetchEpochInfo()
  const res = await PoolUtils.getLiquidityAmountOutFromAmountIn({
    poolInfo,
    slippage: 0,
    inputA: true,
    tickUpper: Math.max(lowerTick, upperTick),
    tickLower: Math.min(lowerTick, upperTick),
    amount: new BN(new Decimal(inputAmount || '0').mul(10 ** poolInfo.mintA.decimals).toFixed(0)),
    add: true,
    amountHasFee: true,
    epochInfo,
  })

  const { execute, extInfo } = await raydium.clmm.openPositionFromBase({
    poolInfo,
    poolKeys,
    tickUpper: Math.max(lowerTick, upperTick),
    tickLower: Math.min(lowerTick, upperTick),
    base: 'MintA',
    ownerInfo: {
      useSOLBalance: true,
    },
    baseAmount: new BN(new Decimal(inputAmount || '0').mul(10 ** poolInfo.mintA.decimals).toFixed(0)),
    otherAmountMax: res.amountSlippageB.amount,
    txVersion,
    // 可选：在此设置优先手续费
    // computeBudgetConfig: {
    //   units: 600000,
    //   microLamports: 100000,
    // },
  })

  const { txId } = await execute({ sendAndConfirm: true })
  console.log('clmm position opened:', { txId, nft: extInfo.nftMint.toBase58() })
}

createPosition()
```

#### 将价格转换为 tick

CLMM 仓位使用 **tick** 来定义价格边界。使用 `TickUtils.getPriceAndTick()` 将人类可读的价格转换为与池的 tick spacing 对齐的有效 tick 值。

```typescript
const { tick } = TickUtils.getPriceAndTick({
  poolInfo,
  price: new Decimal(100),
  baseIn: true, // true = 以 mintB 表示的 mintA 价格
})
```

#### 计算配对数量

在开仓之前，使用 `PoolUtils.getLiquidityAmountOutFromAmountIn()` 来预览在你选择的区间和输入数量下，需要多少另一个 token。

#### 开仓参数

| 参数               | 类型        | 描述                                                   |
| ---------------- | --------- | ---------------------------------------------------- |
| `poolInfo`       | object    | 来自 API 或 RPC 的池信息。                                   |
| `poolKeys`       | object    | 池 keys。devnet 必需。                                    |
| `tickLower`      | number    | 仓位的下 tick 边界。                                        |
| `tickUpper`      | number    | 仓位的上 tick 边界。                                        |
| `base`           | string    | `'MintA'` 或 `'MintB'` ——表示哪个 token `baseAmount` 所指代。 |
| `baseAmount`     | BN        | 要存入的基础 token 数量，以最小单位表示。                             |
| `otherAmountMax` | BN        | 另一个 token 的最大数量（已按滑点调整）。                             |
| `ownerInfo`      | object    | `{ useSOLBalance: true }` 以使用原生 SOL 余额进行 wrapping。   |
| `txVersion`      | TxVersion | 交易版本。                                                |

#### 返回值

所 `extInfo` 对象包含：

| 字段        | 描述              |
| --------- | --------------- |
| `nftMint` | 仓位 NFT mint 公钥。 |

***

### 从流动性开仓

使用 `raydium.clmm.openPositionFromLiquidity()` 作为 `openPositionFromBase()`的替代方案。你不是指定一个 token 数量，而是指定目标流动性值以及两个 token 的最大数量。

```typescript
import {
  ApiV3PoolInfoConcentratedItem,
  TickUtils,
  PoolUtils,
  ClmmKeys,
} from '@raydium-io/raydium-sdk-v2'
import BN from 'bn.js'
import Decimal from 'decimal.js'
import { initSdk, txVersion } from '../config'

const createPositionFromLiquidity = async () => {
  const raydium = await initSdk()

  let poolInfo: ApiV3PoolInfoConcentratedItem
  const poolId = '8sN9549P3Zn6xpQRqpApN57xzkCh6sJxLwuEjcG2W4Ji'
  let poolKeys: ClmmKeys | undefined
  const slippage = 0.025

  const data = await raydium.clmm.getPoolInfoFromRpc(poolId)
  poolInfo = data.poolInfo
  poolKeys = data.poolKeys

  const inputAmount = 0.025
  const [startPrice, endPrice] = [560.979027728865622, 647.86110003403338]

  const { tick: lowerTick } = TickUtils.getPriceAndTick({
    poolInfo,
    price: new Decimal(startPrice),
    baseIn: true,
  })

  const { tick: upperTick } = TickUtils.getPriceAndTick({
    poolInfo,
    price: new Decimal(endPrice),
    baseIn: true,
  })

  const epochInfo = await raydium.fetchEpochInfo()
  const res = await PoolUtils.getLiquidityAmountOutFromAmountIn({
    poolInfo,
    slippage: 0,
    inputA: true,
    tickUpper: Math.max(lowerTick, upperTick),
    tickLower: Math.min(lowerTick, upperTick),
    amount: new BN(new Decimal(inputAmount || '0').mul(10 ** poolInfo.mintA.decimals).toFixed(0)),
    add: true,
    amountHasFee: true,
    epochInfo,
  })

  const { execute, extInfo } = await raydium.clmm.openPositionFromLiquidity({
    poolInfo,
    poolKeys,
    tickUpper: Math.max(lowerTick, upperTick),
    tickLower: Math.min(lowerTick, upperTick),
    liquidity: res.liquidity,
    amountMaxA: new BN(new Decimal(inputAmount || '0').mul(10 ** poolInfo.mintA.decimals).toFixed(0)),
    amountMaxB: new BN(
      new Decimal(res.amountSlippageB.amount.toString()).mul(1 + slippage).toFixed(0)
    ),
    ownerInfo: {
      useSOLBalance: true,
    },
    txVersion,
    // 可选：在此设置优先手续费
    // computeBudgetConfig: {
    //   units: 600000,
    //   microLamports: 10000,
    // },
  })

  const { txId } = await execute({ sendAndConfirm: true })
  console.log('clmm position opened:', { txId, nft: extInfo.address.nftMint.toBase58() })
}

createPositionFromLiquidity()
```

#### 参数（基于流动性）

| 参数           | 类型        | 描述                                                 |
| ------------ | --------- | -------------------------------------------------- |
| `poolInfo`   | object    | 来自 API 或 RPC 的池信息。                                 |
| `poolKeys`   | object    | 池 keys。devnet 必需。                                  |
| `tickLower`  | number    | 仓位的下 tick 边界。                                      |
| `tickUpper`  | number    | 仓位的上 tick 边界。                                      |
| `liquidity`  | BN        | 目标流动性数量。                                           |
| `amountMaxA` | BN        | 要存入的 token A 最大数量（已按滑点调整），以最小单位表示。                 |
| `amountMaxB` | BN        | 要存入的 token B 最大数量（已按滑点调整），以最小单位表示。                 |
| `ownerInfo`  | object    | `{ useSOLBalance: true }` 以使用原生 SOL 余额进行 wrapping。 |
| `txVersion`  | TxVersion | 交易版本。                                              |

***

### 关闭仓位

使用 `raydium.clmm.closePosition()` 来关闭仓位并收回账户租金。该仓位必须具有零流动性、零未领取手续费和零未领取奖励。在关闭之前，先提取全部流动性并收取所有手续费/奖励。

```typescript
import { ApiV3PoolInfoConcentratedItem, ClmmKeys } from '@raydium-io/raydium-sdk-v2'
import { initSdk, txVersion } from '../config'
import { isValidClmm } from './utils'

const closePosition = async () => {
  const raydium = await initSdk()
  let poolInfo: ApiV3PoolInfoConcentratedItem
  const poolId = '2QdhepnKRTLjjSqPL1PtKNwqrUkoLee5Gqs8bvZhRdMv'
  let poolKeys: ClmmKeys | undefined

  if (raydium.cluster === 'mainnet') {
    const data = await raydium.api.fetchPoolById({ ids: poolId })
    poolInfo = data[0] as ApiV3PoolInfoConcentratedItem
    if (!isValidClmm(poolInfo.programId)) throw new Error('target pool is not CLMM pool')
  } else {
    const data = await raydium.clmm.getPoolInfoFromRpc(poolId)
    poolInfo = data.poolInfo
    poolKeys = data.poolKeys
  }

  const allPosition = await raydium.clmm.getOwnerPositionInfo({ programId: poolInfo.programId })
  if (!allPosition.length) throw new Error('user do not have any positions')

  const position = allPosition.find((p) => p.poolId.toBase58() === poolInfo.id)
  if (!position) throw new Error(`user do not have position in pool: ${poolInfo.id}`)

  const { execute } = await raydium.clmm.closePosition({
    poolInfo,
    poolKeys,
    ownerPosition: position,
    txVersion,
  })

  const { txId } = await execute({ sendAndConfirm: true })
  console.log('clmm position closed:', { txId: `https://explorer.solana.com/tx/${txId}` })
}

closePosition()
```

{% hint style="warning" %}
该程序要求 **零流动性、零未领取手续费和零未领取奖励** 才能关闭仓位。最简单的方法是在 `closePosition: true` 中传入 `decreaseLiquidity()` ——SDK 会将减少流动性（其中会收取手续费和奖励）与关闭操作打包为一笔交易。如果单独关闭，请先调用 `harvestAllRewards()` 和 `decreaseLiquidity()` 以将所有余额归零。
{% endhint %}

#### 关闭仓位参数

| 参数              | 类型        | 描述                                 |
| --------------- | --------- | ---------------------------------- |
| `poolInfo`      | object    | 来自 API 或 RPC 的池信息。                 |
| `poolKeys`      | object    | 池 keys。devnet 必需。                  |
| `ownerPosition` | object    | 来自 `getOwnerPositionInfo() 的仓位信息`. |
| `txVersion`     | TxVersion | 交易版本。                              |
