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. The model is the kScript (legacy) engine's; this page names what each mechanic does and what this release does not attach.
What perps mode is
Four mechanics turn on:
- Margin. Every entry commits isolated margin of
notional / leverage; entries that cannot be margined are rejected and counted. - Maker and taker fees. Fills are charged
makerFeePercentortakerFeePercentinstead ofcommissionPercent; see slippage and costs. - Funding. Recorded funding events settle against the open position (
funding: "data", the default) when a funding provider 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
onLiquidationdecides whether the run keeps trading or halts.
A spot run carries none of this: 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).
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.
An entry whose required margin exceeds the available margin is rejected and counted in stats.rejectedOrders. The required amount is fee-inclusive: notional / leverage plus the entry's fee.
Liquidation
The broker maintains an implicit liquidation stop on every open perps position, from the entry fill onward. For a long the level is entry * (1 - 1 / leverage) / (1 - maintenanceMarginPercent / 100); perps and leverage has both formulas and the five scenarios that pin them. Liquidated trades carry exitReason: "liquidation" and the liquidation order is system-owned: it is not in your pending queue and cannot be canceled.
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. |
// Liquidation with on_liquidation "continue": 25x leverage puts the broker's liquidation stop inside an ordinary swing, and trading goes on after each one.
import { input, line, lower, ohlcv, output } from "./sdk/declare";
import { in_close } from "./gen/inputs";
import { emitRow, out_equity } from "./gen/outputs";
import { strategy } from "./gen/strategy";
import { Cross, Sma } from "./sdk/ta";
strategy({ initialCapital: 10000, instrument: "perps", leverage: 25, maintenanceMarginPercent: 0.5, funding: "off", onLiquidation: "continue", qtyType: "fixed", qtyValue: 1, pyramiding: 1 });
input("close", ohlcv.close);
output("equity", line, lower, { description: "Strategy equity" });
const fastMa = new Sma(4);
const slowMa = new Sma(9);
const cross = new Cross();
let crossed: i32 = 0;
export function init(): void {}
export function state(): i32 {
const close = in_close();
const fast = fastMa.update(close);
const slow = slowMa.update(close);
if (isNaN(fast) || isNaN(slow)) return 0;
crossed = cross.update(fast, slow);
return 1;
}
export function finalize(): void {
if (crossed == 1) strategy.long("Long").send();
out_equity(strategy.equity());
emitRow();
}
export function reset(): void {
fastMa.reset();
slowMa.reset();
cross.reset();
crossed = 0;
}At 25x the liquidation level sits about 3.5% under the entry, inside an ordinary swing on most pairs: expect trades closed by liquidation, liquidationCount counting them, and trading continuing after each one. stats.bankruptcyDeficit reports any shortfall a liquidation fill left beyond the position's committed margin; equity floors at zero.
Funding
funding: "data" settles recorded funding events against the open position, and funding: "off" disables funding entirely; only those two literals are accepted. Settlements apply only while a position is open, debit cash and committed margin together (so the liquidation price tightens as margin erodes), and a settlement that depletes margin liquidates the position at that bar's open with zero price PnL. fundingPaid is signed from the strategy's side (positive when the strategy paid), fundingEventsApplied counts the settlements applied, and fundingPaidSeries is the cumulative series.
This release attaches no funding provider on any host. funding: "data" therefore settles nothing and counts every bar an open position could not be settled in fundingUnavailableCount; the backtest card discloses that count on every run it applies to. No rate is ever invented. funding: "off" is the exact no-op it always was, and a strategy that should not depend on funding declares it.
Perps output series
Perps runs 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).
Validation
The settings are validated at build time with the engine's rules: instrument is "spot" or "perps"; leverage more than 0; maintenanceMarginPercent at least 0 and under 100; the fee rates at least 0; funding is "data" or "off"; onLiquidation is "continue" or "halt". A setting linked to a param is checked at init() with the same rules, and an out-of-range value falls back to the default and is reported.