---
title: "Perps: margin, liquidation, funding"
description: "instrument: \"perps\" switches the broker from spot accounting to isolated-margin perpetual futures: leverage, maker and taker fees, funding settlement and…"
order: 125
section: "strategies"
---

<!-- source: docs/indicators/strategies/perps.md; generated by packages/cli/scripts/gen-indicator-docs.ts, do not edit -->

# Perps: margin, liquidation, funding

`instrument: "perps"` switches the broker from spot accounting to isolated-margin perpetual futures: leverage, maker and taker fees, funding settlement and liquidation, each with its own counters, so a result is never quietly shaped by machinery you cannot see. This page names what each mechanic does, with the engine's own formulas, and what the chart does not attach. The five scenarios that pin the numbers are on the [examples](examples.md#the-five-perps-scenarios) page.

## What perps mode changes

Perps mode keeps the same order API and one-net-position model. Four mechanics turn on:

| Mechanic | What changes |
| --- | --- |
| Margin | Every entry commits isolated margin of `notional / leverage`; an entry that cannot be margined is rejected and counted. |
| Maker and taker fees | Fills are charged `makerFeePercent` or `takerFeePercent` instead of `commissionPercent`. |
| Funding | Recorded funding events settle against the open position (`funding: "data"`, the default) when funding data is attached, or funding is off entirely (`funding: "off"`). |
| Liquidation | A position that exhausts its margin is force-closed at the broker's implicit liquidation stop, and `onLiquidation` decides whether the run keeps trading or halts. |

A spot run carries none of this, and spot economics are unchanged until you opt in: the perps stats report zero (`makerFeesPaid`, `takerFeesPaid`, `fundingPaid`, `fundingEventsApplied`, `fundingUnavailableCount`, `liquidationCount` all `0`, `liquidationHalted: false`) and the perps-only series are omitted. The defaults, echoed by a bare declaration: `instrument: "spot"`, `leverage: 1`, `maintenanceMarginPercent: 0.5`, `makerFeePercent: 0`, `takerFeePercent: 0`, `funding: "data"`, `onLiquidation: "continue"`.

## Margin

Each perps entry commits isolated margin equal to the fill's notional divided by `leverage`; `committedMarginSeries` on the output carries the committed total at each bar's close, `0` when flat. Any number above `0` is legal leverage, fractions included; `maintenanceMarginPercent: 0` is legal (the lower bound is inclusive).

| Declared sizing | Under perps |
| --- | --- |
| `qtyType: "percentOfEquity"`, `qtyType: "cash"` | Size the margin commitment. The broker then computes `notional = margin * leverage` and `qty = notional / fillPrice`. |
| `qtyType: "fixed"`, an explicit `.qty(...)` | Direct quantities, margin-checked as `abs(qty) * fillPrice / leverage`. |

**Leverage scales margin, not PnL.** The same signals with the same fixed quantity produce the same `netProfit` as spot, whatever the leverage, until fees or a liquidation differ. Leverage decides how much equity a position ties up and where liquidation sits, not what a trade earns.

```typescript sample=strat-perps-margin-declaration
// Perps margin sizing: percentOfEquity sizes the margin commitment, and notional is that margin times leverage.
strategy({ initialCapital: 10000, instrument: "perps", leverage: 10, maintenanceMarginPercent: 0.5, qtyType: "percentOfEquity", qtyValue: 10, makerFeePercent: 0.02, takerFeePercent: 0.05, slippageBps: 0, funding: "off" });
output("close", line, overlay, { description: "Close price" });

let bars: i32 = 0;

function onBar(): void {
  bars += 1;
  if (bars == 1) strategy.long("MarginLong").limit(100.0).send();
  out_close(bar.close());
}
```

On bars priced near 100, that entry fills at 100 with `qtyType: "percentOfEquity"`, `qtyValue: 10` and `leverage: 10`: the open trade has `qty` 100, `committedMargin` 1000 and `fees` 2, with `makerFeesPaid` 2 and `takerFeesPaid` 0. The 10% sizing setting committed 1000 of margin, not 1000 of notional; the notional was margin times leverage. The `bars` counter is a first-bar gate: the file keeps its own row count.

### Admission

Entries are admitted against realized-basis equity: `committedMargin + entryFee <= availableMargin`, where the available margin excludes unrealized PnL from the open position. A winning open position does not become collateral for adding exposure. An entry whose fee-inclusive requirement (`notional / leverage` plus the entry's fee) exceeds the available margin is rejected and counted in `stats.rejectedOrders`. Reversals stay atomic: the broker simulates the closing leg, then checks whether the new opening leg fits; if it does not, the whole reversal is rejected and the old position remains open.

## Liquidation

```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
```

The broker maintains an implicit liquidation stop on the final open net position, from the entry fill onward. It is not a user order: it is not in your pending queue, cannot be canceled, and closes the trade with `exitReason: "liquidation"` when the bar proves or assumes the level traded. It uses the bar's traded prices, not a mark price the broker does not have.

| Side | Liquidation level |
| --- | --- |
| Long | `P_liq = (Q * E - M) / (Q * (1 - m))` |
| Short | `P_liq = (E + M / Q) / (1 + m)` |

`Q` is the absolute quantity, `E` the average entry, `M` the committed isolated margin and `m` the maintenance margin as a fraction. For a single entry the long level reduces to `entry * (1 - 1 / leverage) / (1 - maintenanceMarginPercent / 100)`: a 10x long entered at 100 with 0.5% maintenance liquidates at `90.45226130653266`, and the short mirror at `109.45273631840797`.

Three cases the bar decides:

- **Entry-bar liquidation** is possible, because a market entry fills at the bar's open and the phase-end check sees the same bar's range.
- **Same-bar fills that change the final position.** The bar cannot prove whether the adverse extreme came before or after the change, so the broker assumes liquidation if the final level is reached, clamps the fill into the bar's traded range, counts it in `ambiguousFillCount` and flags the trade.
- **A gap through the level.** The fill is the bar price, because that is where the market traded; the loss beyond committed margin is `stats.bankruptcyDeficit`, and equity floors at zero.

| `onLiquidation` | Behavior |
| --- | --- |
| `"continue"` (default) | Trading continues; every liquidation counts in `liquidationCount`. |
| `"halt"` | The first liquidation stops the strategy: `liquidationHalted: true`, and every subsequent entry is rejected and counted. |

```typescript sample=strat-liquidation-continue
// Liquidation with on_liquidation "continue": 25x leverage puts the broker's liquidation stop inside an ordinary swing, and trading goes on after each one.
strategy({ initialCapital: 10000, instrument: "perps", leverage: 25, maintenanceMarginPercent: 0.5, funding: "off", onLiquidation: "continue", qtyType: "fixed", qtyValue: 1, pyramiding: 1 });
output("equity", line, lower, { description: "Strategy equity" });

const fastMa = new Sma(4);
const slowMa = new Sma(9);
const cross = new Cross();

function onBar(): void {
  const close = bar.close();
  const fast = fastMa.update(close);
  const slow = slowMa.update(close);
  if (isNaN(fast) || isNaN(slow)) return;
  const crossed = cross.update(fast, slow);
  if (crossed == 1) strategy.long("Long").send();
  out_equity(strategy.equity());
}
```

At 25x the liquidation level sits about 3.5% under the entry, inside an ordinary swing on most pairs: expect trades closed by `liquidation` (**Liquidated** under **Exit via** in the Strategy Tester's Trades tab), `liquidationCount` counting them, and trading continuing after each one.

## Maker and taker fees

Perps ignores `commissionPercent`: a fill pays `makerFeePercent` when it is limit-bound and `takerFeePercent` when it crosses the market. [Slippage and costs](slippage-and-costs.md#maker-and-taker-fees-perps) classifies every fill, shows a worked example, and covers what happens to the other instrument's fee settings.

## Funding

`funding: "data"` settles recorded funding events against the open position, and `funding: "off"` disables funding entirely; only those two literals are accepted. The rate is decimal (1 bp is `0.0001`), and positive rates mean longs pay and shorts receive. `fundingPaid` is signed from the strategy's side (positive when the strategy paid), `fundingEventsApplied` counts the settlements applied, and `fundingPaidSeries` is the cumulative series.

Funding is a drip against isolated margin. Settlements apply only while a position is open, and debit cash and committed margin together, so the liquidation line moves closer with every settlement paid. A settlement that erodes committed margin to zero or below liquidates the position at that bar's open with zero price PnL, and later same-bar fills cannot rescue it.

The chart attaches no funding data to a strategy run. `funding: "data"` therefore settles nothing and counts every bar an open position could not be settled in `fundingUnavailableCount`: partial coverage is counted, never charged, and no rate is ever invented. The Strategy Tester discloses the count on every run it applies to: **Run details** read "Funding data did not cover N bars", and the Overview's **Funding** chart says "No funding was applied on this run." `funding: "off"` is the exact no-op it always was, and a strategy that should not depend on funding declares it.

## Perps stats and series

A run's stats always carry the perps fields, zero on spot; the [stats reference](stats-reference.md#perps) defines each exactly.

| Stat | Meaning |
| --- | --- |
| `makerFeesPaid`, `takerFeesPaid` | Fees by fill class. |
| `fundingPaid` | Net funding settled, signed from the strategy's side. |
| `fundingEventsApplied` | How many funding settlements the run applied. |
| `fundingUnavailableCount` | Bars with an open position and no settlement data. |
| `liquidationCount` | Trades force-closed by the liquidation stop. |
| `liquidationHalted` | `true` once `onLiquidation: "halt"` stopped the run. |
| `bankruptcyDeficit` | How far equity would have gone below zero before the floor. |

Perps runs also add two bar-aligned series to the output, present on every perps run and omitted on spot: `committedMarginSeries` (margin committed to the open position at each bar close) and `fundingPaidSeries` (cumulative funding paid, ending at `stats.fundingPaid`). The Strategy Tester draws them as the Overview's **Margin** and **Funding** charts, each with **Show on chart**, shows the fee split and the funding in the Performance tab, and the liquidations in the Overview and the Trades tab.

## Validation

The settings are checked with the engine's rules as the file builds, and a value out of range stops the build with the problem in the editor's **Console**. A setting linked to a param is checked when the run starts, with the same rules, and an out-of-range value falls back to the default.

| Setting | Rule |
| --- | --- |
| `instrument` | `"spot"` or `"perps"` |
| `leverage` | More than 0 |
| `maintenanceMarginPercent` | At least 0 and under 100 |
| `makerFeePercent`, `takerFeePercent` | At least 0 |
| `funding` | `"data"` or `"off"` |
| `onLiquidation` | `"continue"` or `"halt"` |
