---
title: "Backtest a Hyperliquid Perp Strategy"
description: "Hyperliquid trades as perpetuals: leveraged, funded and liquidatable. A backtest that ignores those three facts will happily approve a strategy the venue would…"
order: 88
section: "strategies"
---

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

# Backtest a Hyperliquid Perp Strategy

Hyperliquid trades as perpetuals: leveraged, funded and liquidatable. A backtest that ignores those three facts will happily approve a strategy the venue would have destroyed. This is the kScript (legacy) walkthrough as one Indicator file: a trend strategy under perps accounting, isolated margin at 10x, maker and taker fees, and a protective stop off the average entry.

## The strategy

```typescript
// A funding-aware trend strategy at 10x: isolated margin, maker and taker rates, recorded funding, and a stop 3% under the average entry.
import { input, line, ohlcv, output, overlay } from "./sdk/declare";
import { in_close } from "./gen/inputs";
import { emitRow, out_fast, out_slow } from "./gen/outputs";
import { strategy } from "./gen/strategy";
import { Cross, Ema } from "./sdk/ta";

strategy({ initialCapital: 10000, instrument: "perps", leverage: 10, qtyType: "percentOfEquity", qtyValue: 10, makerFeePercent: 0.015, takerFeePercent: 0.045, funding: "data", slippageBps: 2 });
input("close", ohlcv.close);
output("fast", line, overlay, { description: "21-period EMA of close" });
output("slow", line, overlay, { description: "55-period EMA of close" });

const fastEma = new Ema(21);
const slowEma = new Ema(55);
const cross = new Cross();
let fast: f64 = NaN;
let slow: f64 = NaN;
let crossed: i32 = 0;

export function init(): void {}

export function state(): i32 {
  fast = fastEma.update(in_close());
  slow = slowEma.update(in_close());
  if (isNaN(fast) || isNaN(slow)) return 0;
  crossed = cross.update(fast, slow);
  return 1;
}

export function finalize(): void {
  if (crossed == 1) strategy.long("Trend").send();
  if (crossed == -1) strategy.closeAll();
  if (strategy.positionSize() > 0) strategy.exit("Protect").from("Trend").stop(strategy.positionAvgPrice() * 0.97).send();
  out_fast(fast);
  out_slow(slow);
  emitRow();
}

export function reset(): void {
  fastEma.reset();
  slowEma.reset();
  cross.reset();
  fast = NaN;
  slow = NaN;
  crossed = 0;
}
```

```bash
om wrun install ./hl-funding-trend
om backtest @you/hl-funding-trend --asset HYPERLIQUID_FUTURES:BTC --window 90d
```

The price input carries no market pin: a strategy trades the market it is run on, and `--asset` names it. Reading the declaration, which does most of the perps work:

- `instrument: "perps"` with `leverage: 10`: sizing commits **margin**, not notional. `qtyValue: 10` means each entry commits 10% of equity as isolated margin; the notional is that margin times leverage. The broker tracks the liquidation price from your leverage and maintenance margin and closes you there if a bar proves or assumes the level traded.
- `makerFeePercent: 0.015, takerFeePercent: 0.045`: fees route by fill type, so market entries, stops and `closeAll` pay taker while limit-bound fills pay maker. Set your own tier's numbers; they ship with the package.
- `funding: "data"`: the broker settles recorded funding against the open position when a funding provider is attached. This release attaches none on any host, so the run counts every unsettled open bar in `fundingUnavailableCount` and charges nothing; the number is the disclosure until funding data lands, and `funding: "off"` declares a strategy that should not depend on it.
- `strategy.positionAvgPrice() * 0.97`: the stop tracks the average entry, re-armed every held bar.

## Reading the result

Three lines to check before believing the equity curve:

1. **`fundingUnavailableCount`** in the stats and the card's note: how many held bars went unsettled. A long that looks fine gross can bleed through settlements while it holds, and this run has not charged them.
2. **`liquidationCount`** and trades whose exit says `liquidation`: at 10x, a 3% protective stop and the liquidation level are uncomfortably close neighbors. If liquidations show up, the venue closed you before your stop did.
3. **`ambiguousFillCount`**: fills the declared fill model settled where one bar touched the stop and a level; the run details read `bar resolution` in this release.

## Tune it like it is real

- **Leverage down first.** At 5x the liquidation level sits twice as far; watch `liquidationCount` go to zero before you tune anything else.
- **Widen the stop or drop the interval.** Brackets arm on the bar after entry, so a tight stop on a coarse chart is exposed for one full bar; [fill simulation](fill-simulation.md) covers the trade-off.
- **Run the spot twin.** Copy the file, drop the perps settings, and put both on the chart: compare mode shows what leverage and fees cost you. That difference is the part most backtests never model.
