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 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 commission. |
| 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: "percent, 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.
// 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
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 awayThe 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
ambiguousFillCountand 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 liquidation. |
"halt" | The first liquidation stops the strategy: liquidation, and every subsequent entry is rejected and counted. |
// 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 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 defines each exactly.
| Stat | Meaning |
|---|---|
makerFeesPaid, takerFeesPaid | Fees by fill class. |
fundingPaid | Net funding settled, signed from the strategy's side. |
funding | How many funding settlements the run applied. |
funding | Bars with an open position and no settlement data. |
liquidation | Trades force-closed by the liquidation stop. |
liquidation | true once onLiquidation: "halt" stopped the run. |
bankruptcy | 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 |
maintenance | At least 0 and under 100 |
makerFeePercent, takerFeePercent | At least 0 |
funding | "data" or "off" |
onLiquidation | "continue" or "halt" |