---
title: Perps and Leverage
description: Declare perps strategies, understand leveraged margin sizing, liquidation, maker/taker fees, funding, and the perps-specific stats.
---

## What perps mode changes

Set `instrument="perps"` on `strategy()` when the backtest should use isolated leveraged perpetual accounting instead of spot accounting. Perps mode keeps the same order API and one-net-position model, but it changes how default sizing is interpreted, how entries are admitted, how fees are charged, and how open positions can be liquidated or debited by funding.

```javascript title="perps-declaration.ks"
//@version=2
strategy(title="Perps Margin Declaration", initialCapital=10000, instrument="perps", leverage=10, maintenanceMarginPercent=0.5, qtyType="percentOfEquity", qtyValue=10, makerFeePercent=0.02, takerFeePercent=0.05, slippageBps=0, funding="off")

timeseries bars = ohlcv(symbol=currentSymbol, exchange=currentExchange)

if (barIndex == 0) {
  strategy.entry("MarginLong", "long", limit=100)
}

plotLine(value=bars.close, width=1, colors=["#4f8cff"], label=["Close"], desc=["Close price"])
```

That probe enters at 100 with `qtyType="percentOfEquity"`, `qtyValue=10`, and `leverage=10`. The output open trade has `qty=100`, `committedMargin=1000`, `fees=2`, `stats.makerFeesPaid=2`, and `stats.takerFeesPaid=0`. In other words, the 10% sizing setting committed 1000 margin, not 1000 notional; notional was margin times leverage.

## Margin sizing and admission

For perps, `qtyType="percentOfEquity"` and `qtyType="cash"` size the margin commitment. The engine then computes `notional = margin * leverage` and `qty = notional / fillPrice`. `qtyType="fixed"` and an explicit `strategy.entry(..., qty=...)` are direct quantities instead: they are margin-checked as `abs(qty) * fillPrice / leverage`.

Entries are admitted against realized-basis equity. The check is `committedMargin + entryFee <= availableMargin`, where available margin excludes unrealized PnL from the open position. A winning open position does not become collateral for adding more exposure. Reversals remain atomic: the broker first simulates the closing leg, then checks whether the new opening leg fits. If it does not fit, the whole reversal is rejected and the old position remains open.

## Liquidation price

The geometry in one picture: leverage decides how much price movement your margin can absorb before the exchange closes you.

```text
   long 1 unit at 100, leverage 10, maintenance 0.5%

   entry 100 -+------------------------------------
              |  committed margin = 100/10 = 10
              |  the price may fall ~9.55 before
              |  margin (less maintenance) is gone
   P_liq  ----+---- 90.4522...  <- liquidation
              |
              +-- lower leverage pushes this line further away
```


Perps mode maintains an implicit system liquidation stop for the final open net position. It is not a user order, cannot be canceled, and closes with `exitReason="liquidation"` when the bar proves or assumes that the liquidation level traded.

For a long, the isolated liquidation level is:

```text
P_liq = (Q * E - M) / (Q * (1 - m))
```

For a short, it is:

```text
P_liq = (E + M / Q) / (1 + m)
```

`Q` is absolute quantity, `E` is average entry, `M` is committed isolated margin, and `m` is `maintenanceMarginPercent / 100`. With a single 10x long entered at 100 and `maintenanceMarginPercent=0.5`, the liquidation probe exits at `90.45226130653266`.

```javascript title="perps-liquidation.ks"
//@version=2
strategy(title="Perps Liquidation Anchor", initialCapital=10000, instrument="perps", leverage=10, maintenanceMarginPercent=0.5, qtyType="fixed", qtyValue=1, slippageBps=0, makerFeePercent=0, takerFeePercent=0, funding="off")

timeseries bars = ohlcv(symbol=currentSymbol, exchange=currentExchange)

if (barIndex == 0) {
  strategy.entry("L", "long")
}

plotLine(value=bars.close, width=1, colors=["#dc2626"], label=["Close"], desc=["Close price"])
```

The probe output records one trade with `entryPrice=100`, `exitReason="liquidation"`, `exitPrice=90.45226130653266`, `liquidationCount=1`, and `bankruptcyDeficit=0`. Entry-bar liquidation is possible because the market entry fills at the bar open and the phase-end liquidation check sees the same bar's range.

The short-side fixture, `perps-short-liquidation-bars.json`, uses the same entry price, leverage, and maintenance rate. For `E=100`, `M=10`, `Q=1`, and `m=0.005`, the short formula gives `109.45273631840797`; the probe trades through that level, exits there, and records `takerFeesPaid=0.05472636815920399` with `makerFeesPaid=0`. Because the entry is a limit fill with maker fee set to zero, the nonzero taker total comes from the liquidation fill.

When fills on the same bar change the final position, the engine assumes liquidation if that final liquidation level is reached and marks the fill as ambiguous. The `perps-ambiguous-liquidation.ks` output records `ambiguousFillCount=1`; its final long `P_liq` is `122.625`, outside the fixture bar's `90` to `120` traded range, so the liquidation fill is clamped to the bar high and recorded at `120`.

```javascript title="perps-short-liquidation-taker.ks"
//@version=2
strategy(title="Perps Short Liquidation Taker", initialCapital=10000, instrument="perps", leverage=10, maintenanceMarginPercent=0.5, qtyType="fixed", qtyValue=1, slippageBps=0, makerFeePercent=0, takerFeePercent=0.05, funding="off")

timeseries bars = ohlcv(symbol=currentSymbol, exchange=currentExchange)

if (barIndex == 0) {
  strategy.entry("S", "short", limit=100)
}

plotLine(value=bars.close, width=1, colors=["#be123c"], label=["Close"], desc=["Close price"])
```

## Maker and taker fees

Perps ignores `commissionPercent`; use `makerFeePercent` and `takerFeePercent` instead. Limit-bound fills pay maker fees: limit entries, take-profit limit legs, and the flattening leg of a reversal triggered by a limit entry. Fills that cross the market pay taker fees: market entries, stop entries, protective stops, trailing stops, signal exits, `closeAll`, liquidation, and reversal flattening legs triggered by market or stop entries.

Spot scripts are unchanged. In spot mode, `commissionPercent` is active and maker/taker params are ignored. In perps mode, nonzero `commissionPercent` is ignored and the engine emits a warning so the result does not silently double-charge. The warning probes record `STRATEGY_PERPS_COMMISSION_IGNORED` for `perps-commission-warning.ks` and `STRATEGY_SPOT_MAKER_TAKER_IGNORED` for `spot-maker-taker-warning.ks`.

## Funding

Funding is a drip against your isolated margin, and the liquidation line moves closer with every settlement you pay:

```text
   committed margin over a held long (positive rates)

   10 |#########
    9 |         #########          each settlement debits
    8 |                  ######### cash AND margin together
    7 |                           #########
    0 +--------|--------|--------|--------|---- bars
            settle   settle   settle   depleting settle
                                        -> liquidates at
                                           that bar's open
```


With `funding="data"` on a perps strategy, the engine consumes settlement events from recorded funding rates from your chart's venue. The settlement rate is decimal, so 1 bp is `0.0001`. Positive rates mean longs pay and shorts receive; negative rates reverse that direction. `stats.fundingPaid` is signed in account currency, with positive meaning the strategy paid funding.

```javascript title="perps-funding.ks"
//@version=2
strategy(title="Perps Funding Data", initialCapital=10000, instrument="perps", funding="data", leverage=10, maintenanceMarginPercent=0.5, qtyType="fixed", qtyValue=1, slippageBps=0, makerFeePercent=0, takerFeePercent=0)

timeseries bars = ohlcv(symbol=currentSymbol, exchange=currentExchange)

if (barIndex == 0) {
  strategy.entry("L", "long")
}

plotLine(value=strategy.equity(), width=1, colors=["#0f766e"], label=["Equity"], desc=["Strategy equity"])
```

The probe attaches this serializable provider fixture:

```json title="perps-funding-provider.json"
{
  "events": [
    {
      "time": 1700009000000,
      "rate": 0.01
    }
  ],
  "coverage": {
    "start": 1700000000000,
    "end": 1700010800000
  }
}
```

The output records `fundingPaid=1`, `fundingUnavailableCount=1`, and an open trade whose `committedMargin` moved from 10 to 9. Funding settles against isolated margin, so it moves the liquidation price. The short-side direction probe, `perps-short-funding.ks`, uses the same positive-rate provider on a short position and records `fundingPaid=-1`, meaning the strategy received funding.

If funding erodes committed margin to zero or below, the bar still has one phase-end liquidation evaluation, but the bar-open settlement forces that outcome: the prior position liquidates at the settlement bar's open, and later same-bar fills cannot rescue it. The depletion probe uses `perps-funding-depletion-bars.json` and `perps-funding-depletion-provider.json`, where a `0.1` rate settles at `1700007200000`; the recorded trade exits on `exitBar=2` at that bar's `100` open with `exitReason="liquidation"`, `fundingPaid=10`, and `liquidationCount=1`.

```javascript title="perps-funding-depletion.ks"
//@version=2
strategy(title="Perps Funding Depletion", initialCapital=10000, instrument="perps", funding="data", leverage=10, maintenanceMarginPercent=0.5, qtyType="fixed", qtyValue=1, slippageBps=0, makerFeePercent=0, takerFeePercent=0)

timeseries bars = ohlcv(symbol=currentSymbol, exchange=currentExchange)

if (barIndex == 0) {
  strategy.entry("L", "long")
}

plotLine(value=strategy.equity(), width=1, colors=["#0e7490"], label=["Equity"], desc=["Strategy equity"])
```

Set `funding="off"` to make funding an exact no-op. Missing or partial funding coverage is counted, not charged. A complete provider range with no events is known zero funding.

## Perps stats

The Strategy Tester always emits the perps stats, even for spot runs where they are zero. `makerFeesPaid` and `takerFeesPaid` are account-currency fee totals by fill class. `fundingPaid` is signed account-currency funding, positive when the strategy paid and negative when it received. `fundingUnavailableCount` counts bars where a perps position was open and funding coverage could not prove the settlement window. `liquidationCount` counts system liquidation events. `bankruptcyDeficit` records how far equity would have gone below zero before the engine floored it at zero.

The bankruptcy gap probe uses `perps-bankruptcy-gap-bars.json`, starts with `initialCapital=10`, enters one 10x long at 100, then gaps the next bar to 80. It records `exitReason="liquidation"`, `exitPrice=80`, `netProfit=-20`, and `bankruptcyDeficit=10`.

```javascript title="perps-bankruptcy-gap.ks"
//@version=2
strategy(title="Perps Bankruptcy Gap", initialCapital=10, instrument="perps", leverage=10, maintenanceMarginPercent=0.5, qtyType="fixed", qtyValue=1, slippageBps=0, makerFeePercent=0, takerFeePercent=0, funding="off")

timeseries bars = ohlcv(symbol=currentSymbol, exchange=currentExchange)

if (barIndex == 0) {
  strategy.entry("L", "long")
}

plotLine(value=strategy.equity(), width=1, colors=["#7c2d12"], label=["Equity"], desc=["Strategy equity"])
```

## Not bugs

These are intentional parts of the model. `commissionPercent` does nothing under `instrument="perps"` and the warning is expected. Missing funding rows are counted in `fundingUnavailableCount` and not charged. Liquidation uses the chart's last-traded OHLC, not mark price, because the strategy engine does not have mark-price data. Spot scripts and spot economics are unchanged unless you opt into `instrument="perps"`.

## The five canonical scenarios

Five scripts define what correct perps behavior means, and they exist in three places at once: as strategy templates in the editor (the `Perps:` entries in the template picker), as the executed examples behind the numbers on this page, and as an acceptance battery inside the engine's own test suite. Every expected value in that battery is derived by hand from the formulas on this page, never from engine output, and the engine cannot ship a release unless all five reproduce to the last digit.

| Scenario | Template | What it pins down | The number it must land on | Executed probe |
| --- | --- | --- | --- | --- |
| 1. Long liquidation | Perps: Long Liquidation | The long liquidation price formula | Entry 100 at 10x, 0.5% maintenance liquidates at `90.45226130653266` | `perps-liquidation.ks` |
| 2. Short liquidation with fees | Perps: Short with Taker Fees | The short formula plus taker fees on the market entry and the liquidation fill | Entry 100 liquidates at `109.45273631840797`, total fees `0.104726368159204` | `perps-acceptance-short-taker.ks` |
| 3. Maker and taker split | Perps: Maker and Taker Split | Fee class per fill: limit legs maker, market and stop legs taker | `makerFeesPaid 0.02`, `takerFeesPaid 0.096`, net `1.8840000000000003` | `perps-maker-taker-split.ks` |
| 4. Funding erosion | Perps: Funding Erosion | Funding debits cash and committed margin; depletion liquidates at the bar open | Three 1% settlements walk margin 10 to 7; a 7% settlement liquidates with `fundingPaid 10` and zero price pnl | `perps-acceptance-funding-erosion.ks` |
| 5. Bankruptcy gap | Perps: Bankruptcy Gap | Gap-through fills at the worse bar price; deficit accounting | A gap to 80 books `bankruptcyDeficit 10` and equity floors at exactly `0` | `perps-bankruptcy-gap.ks` |

Each scenario is an executed probe script with its bar fixtures, and its outputs are recorded in the observed artifacts beside it. Scenario 2 differs from the short-formula example earlier on this page on purpose: that example enters with a zero-fee limit fill to isolate the formula, while the canonical scenario uses a market entry so both legs pay taker. Scenario 4 likewise complements the single-settlement depletion example with a gradual erosion before the depleting settlement.

One design note the third scenario teaches: the script phase runs at bar close, after that bar's fills, so a flat-position entry guard sees the post-exit state on the exit bar itself. Keep entry zones disjoint from your exit prices or the strategy re-arms on the very bar it exited; a consumed entry order never refills, but a re-placed one is a new order.
