How to Close Positions
This guide shows you how to close both taker and maker positions using the PerpCity SDK.
Closing Taker Positions
To close a taker position (long or short):
import { closePosition } from '@strobelabs/perpcity-sdk'
await closePosition(
context,
'0x...', // perpId (Hex)
123n, // positionId (bigint)
{
minAmt0Out: 0, // Minimum token0 output (slippage protection)
minAmt1Out: 0, // Minimum token1 output
maxAmt1In: 1000000 // Maximum token1 input
}
)That's it! The SDK handles:
- Calculating your PnL
- Settling funding payments
- Returning your margin (if profitable)
- Closing the position on-chain
Closing Maker Positions
Closing a maker position is the same:
import { closePosition } from '@strobelabs/perpcity-sdk'
await closePosition(
context,
'0x...', // perpId (Hex)
456n, // positionId (bigint)
{
minAmt0Out: 0,
minAmt1Out: 0,
maxAmt1In: 1000000
}
)This withdraws your liquidity and any earned fees.
Complete Example
import { PerpCityContext, closePosition } from '@strobelabs/perpcity-sdk'
import { createWalletClient, http } from 'viem'
import { base } from 'viem/chains'
import { privateKeyToAccount } from 'viem/accounts'
async function closeMyPosition() {
// Setup wallet
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`)
const walletClient = createWalletClient({
account,
chain: base,
transport: http(process.env.RPC_URL)
})
// Initialize context
const context = new PerpCityContext({
walletClient,
rpcUrl: process.env.RPC_URL!,
deployments: {
perpManager: process.env.PERP_MANAGER_ADDRESS as `0x${string}`,
usdc: process.env.USDC_ADDRESS as `0x${string}`
}
})
// Close position
console.log('Closing position...')
await closePosition(
context,
'0x...', // perpId
123n, // positionId
{
minAmt0Out: 0,
minAmt1Out: 0,
maxAmt1In: 1000000
}
)
console.log('Position closed successfully!')
}
closeMyPosition()Understanding Close Settlement
When you close a position:
For Taker Positions
Final Balance = Initial Margin + PnL - Funding Payments - FeesFor Maker Positions
Final Balance = Initial Liquidity + Fee Earnings + Funding Received- You receive your initial liquidity back
- Plus any earned trading fees
- Plus/minus funding payments based on market skew
Lockup Periods (Maker Positions)
Maker positions may have minimum lockup periods:
const config = await context.getPerpConfig('0x...')
const lockupModule = config.lockupPeriod
console.log('Lockup period module:', lockupModule)If you try to close during lockup:
Error: Position still lockedSolution: Wait until the lockup period expires.
Checking Position Age
To check if lockup has expired:
import { createPublicClient, http } from 'viem'
import { base } from 'viem/chains'
const publicClient = createPublicClient({
chain: base,
transport: http(process.env.RPC_URL)
})
// Get position open event
const logs = await publicClient.getContractEvents({
address: perpManagerAddress,
abi: perpManagerAbi,
eventName: 'PositionOpened',
args: {
positionId: 123n
}
})
const openBlock = logs[0].blockNumber
const openBlockData = await publicClient.getBlock({ blockNumber: openBlock })
const openTimestamp = openBlockData.timestamp
const currentBlock = await publicClient.getBlock()
const currentTimestamp = currentBlock.timestamp
const positionAge = Number(currentTimestamp - openTimestamp)
const lockupPeriod = config.lockupPeriod
if (positionAge >= lockupPeriod) {
console.log('Position can be closed')
} else {
const waitTime = lockupPeriod - positionAge
console.log(`Wait ${waitTime} more seconds`)
}Handling Liquidations
If your position is liquidatable, it may be liquidated by a third party before you can close it:
import { getPositionIsLiquidatable } from '@strobelabs/perpcity-sdk'
// Step 1: Fetch position data
const positionData = await context.getOpenPositionData(
'0x...', // perpId
123n, // positionId
true, // isLong
false // isMaker
)
// Step 2: Check if liquidatable
const isLiquidatable = getPositionIsLiquidatable(positionData)
if (isLiquidatable) {
console.warn('WARNING: Position is liquidatable!')
// Close immediately or add margin
}Common Errors
Position Not Found
Error: Position does not existSolution: Verify position ID and perpId are correct. Position may already be closed.
Position Already Closed
Error: Position already closedSolution: Check if you already closed this position or if it was liquidated.
Still in Lockup
Error: Position still lockedSolution: Wait for lockup period to expire (maker positions only).
Liquidated Position
Error: Position has been liquidatedSolution: Position was liquidated due to insufficient margin. Cannot close.
Transaction Failed Errors
If the transaction fails:
- Check gas: Ensure you have enough ETH for gas
- Check network: Verify you're on the correct network (Base)
- Check RPC: Try a different RPC endpoint if timeout occurs
Batch Closing Multiple Positions
To close multiple positions efficiently:
// Close positions sequentially
const positionIds = [123n, 124n, 125n]
const perpId = '0x...'
for (const positionId of positionIds) {
await closePosition(context, perpId, positionId, {
minAmt0Out: 0,
minAmt1Out: 0,
maxAmt1In: 1000000
})
console.log(`Closed position ${positionId}`)
}
// Or in parallel (riskier, may hit rate limits)
await Promise.all(
positionIds.map(positionId =>
closePosition(context, perpId, positionId, {
minAmt0Out: 0,
minAmt1Out: 0,
maxAmt1In: 1000000
})
)
)