The five canonical perps scripts behind the kScript (legacy) engine's acceptance battery, ported as Indicator files: liquidation on both sides, fee classification, funding erosion and bankruptcy accounting. They are reproduction scripts: each re-derives its pinned number on the fixture bars committed beside the engine's probe (prices near 100), which is what makes it verifiable; on a live chart they simply enter at that chart's prices. The perps and leverage page has the formulas and the numbers each must land on. kScript's barIndex == 0 gate becomes a row counter the file keeps itself.
1. Long liquidation
A 10x long entered at 100 with 0.5% maintenance margin must liquidate at exactly (100 - 10) / 0.995 = 90.45226130653266.
// Scenario 1, long liquidation: a 10x long at 100 with 0.5% maintenance margin must liquidate at (100 - 10) / 0.995.
import { input, line, ohlcv, output, overlay } from "./sdk/declare";
import { in_close } from "./gen/inputs";
import { emitRow, out_close } from "./gen/outputs";
import { strategy } from "./gen/strategy";
strategy({ initialCapital: 10000, instrument: "perps", leverage: 10, maintenanceMarginPercent: 0.5, qtyType: "fixed", qtyValue: 1, slippageBps: 0, makerFeePercent: 0, takerFeePercent: 0, funding: "off" });
input("close", ohlcv.close);
output("close", line, overlay, { description: "Close price" });
let bars: i32 = 0;
let close: f64 = NaN;
export function init(): void {}
export function state(): i32 {
close = in_close();
bars += 1;
return 1;
}
export function finalize(): void {
if (bars == 1) strategy.long("L").send();
out_close(close);
emitRow();
}
export function reset(): void {
bars = 0;
close = NaN;
}2. Short liquidation with taker fees
The short side of the same formula, (100 + 10) / 1.005 = 109.45273631840797, with a market entry so both the entry fill and the liquidation fill pay takerFeePercent.
// Scenario 2, short liquidation with taker fees: the short formula (100 + 10) / 1.005, both fills paying the taker rate.
import { input, line, ohlcv, output, overlay } from "./sdk/declare";
import { in_close } from "./gen/inputs";
import { emitRow, out_close } from "./gen/outputs";
import { strategy } from "./gen/strategy";
strategy({ initialCapital: 10000, qtyType: "fixed", qtyValue: 1, instrument: "perps", leverage: 10, maintenanceMarginPercent: 0.5, takerFeePercent: 0.05, funding: "off" });
input("close", ohlcv.close);
output("close", line, overlay, { description: "Close price" });
let close: f64 = NaN;
export function init(): void {}
export function state(): i32 {
close = in_close();
return 1;
}
export function finalize(): void {
if (strategy.positionSize() == 0 && close < 100.5) strategy.short("S").send();
out_close(close);
emitRow();
}
export function reset(): void {
close = NaN;
}3. Maker and taker split
Two trades, four fills, four fee classifications: a limit entry and its take-profit limit pay maker; a market entry and its protective stop pay taker. Also a guard-design lesson: the file runs at the bar's close, after fills, so entry zones stay disjoint from exit prices or the flat-position guard re-arms on the very bar an exit filled.
// Scenario 3, maker and taker split: a limit entry and its take-profit pay maker, a market entry and its stop pay taker.
import { input, line, ohlcv, output, overlay } from "./sdk/declare";
import { in_close } from "./gen/inputs";
import { emitRow, out_close } from "./gen/outputs";
import { strategy } from "./gen/strategy";
strategy({ initialCapital: 10000, qtyType: "fixed", qtyValue: 1, instrument: "perps", leverage: 5, makerFeePercent: 0.01, takerFeePercent: 0.05, funding: "off" });
input("close", ohlcv.close);
output("close", line, overlay, { description: "Close price" });
let close: f64 = NaN;
let held: f64 = 0;
export function init(): void {}
export function state(): i32 {
close = in_close();
held = strategy.positionSize();
return 1;
}
export function finalize(): void {
if (held == 0 && close > 100.5 && close < 103.0) strategy.long("LimitIn").limit(95.0).send();
if (held == 0 && close > 110.5) strategy.long("MarketIn").send();
if (held > 0) {
strategy.exit("TP").from("LimitIn").limit(105.0).send();
strategy.exit("SL").from("MarketIn").stop(92.0).send();
}
out_close(close);
emitRow();
}
export function reset(): void {
close = NaN;
held = 0;
}4. Funding erosion
Funding settles against the isolated margin: each settlement debits cash and committed margin together, 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. This release attaches no funding provider, so the port counts every unsettled open bar in fundingUnavailableCount and charges nothing; the numbers reproduce once a provider lands.
// Scenario 4, funding erosion: settlements debit cash and committed margin together, and a depleting settlement liquidates at that bar's open.
import { input, line, ohlcv, output, overlay } from "./sdk/declare";
import { in_close } from "./gen/inputs";
import { emitRow, out_close } from "./gen/outputs";
import { strategy } from "./gen/strategy";
strategy({ initialCapital: 10000, qtyType: "fixed", qtyValue: 1, instrument: "perps", leverage: 10, maintenanceMarginPercent: 0.5, funding: "data" });
input("close", ohlcv.close);
output("close", line, overlay, { description: "Close price" });
let close: f64 = NaN;
export function init(): void {}
export function state(): i32 {
close = in_close();
return 1;
}
export function finalize(): void {
if (strategy.positionSize() == 0 && close > 99.5) strategy.long("L").send();
out_close(close);
emitRow();
}
export function reset(): void {
close = NaN;
}5. Bankruptcy gap
Price gaps straight through the liquidation level: the fill is the bar price because it is worse, the loss beyond committed margin is recorded as bankruptcyDeficit, and equity floors at exactly zero.
// Scenario 5, bankruptcy gap: a gap through the liquidation level fills at the worse bar price, the loss past the margin is the bankruptcy deficit, and equity floors at zero.
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";
strategy({ initialCapital: 10, instrument: "perps", leverage: 10, maintenanceMarginPercent: 0.5, qtyType: "fixed", qtyValue: 1, slippageBps: 0, makerFeePercent: 0, takerFeePercent: 0, funding: "off" });
input("close", ohlcv.close);
output("equity", line, lower, { description: "Strategy equity" });
let bars: i32 = 0;
export function init(): void {}
export function state(): i32 {
bars += 1;
return 1;
}
export function finalize(): void {
if (bars == 1) strategy.long("L").send();
out_equity(strategy.equity());
emitRow();
}
export function reset(): void {
bars = 0;
}