# Ladder

Place limit orders at evenly spaced prices across a range in one request, and know how levels are priced, sized, placed, and how a ladder ends.

A ladder places several GTC limit orders, called levels, at evenly spaced prices between a minimum and a maximum. Other venues call this a scaled order. Use it to spread an entry or an exit across a price range in one request, with [`CreateTrigger`](https://testnet.polyester.com/docs/api-docs/connect/triggers.v1.TriggersService/CreateTrigger) ([`POST /v1/triggers`](https://testnet.polyester.com/docs/api-docs/rest/POST/v1/triggers)) and a `ladder` strategy. A ladder does not watch a price: all its levels are placed when it is created.

## Fields

| Field                    | Rule                                                           |
| ------------------------ | -------------------------------------------------------------- |
| `qty_scaled`             | Required, positive. The total quantity of all levels           |
| `ladder.side`            | Required: `BUY` or `SELL`                                      |
| `ladder.price_min_ticks` | Required, positive, and on the Pair's tick size                |
| `ladder.price_max_ticks` | Required, above `price_min_ticks`, and on the Pair's tick size |
| `ladder.levels`          | Required, `2` to `100`                                         |
| `ladder.post_only`       | Optional. Each level is rejected instead of taking liquidity   |

Levels are always distributed linearly. `ladder_state.ladder_distribution` reports `LINEAR`; other distributions cannot be requested. In REST, the range is `priceMin` and `priceMax` and the quantity `qty`, as decimal strings.

## How levels are priced and sized

- **Prices.** The levels are spread evenly from `price_min_ticks` to `price_max_ticks`, both included, on the Pair's tick grid. Neighbouring levels are at least one tick apart, so the range must hold at least `levels - 1` ticks.
- **Quantities.** Each level gets the total divided by the number of levels, rounded down to the Pair's quantity step. The last level, at the maximum price, also takes the remainder.
- **Small totals.** If the total divided by the number of levels rounds down to zero, only the last level is placed, with the whole quantity.

Every level must also meet the Pair's minimum order rules on its own.

## Placement is all or nothing

Polyester checks and places every level when it accepts the ladder. If any level cannot be placed, for example because the balance cannot fund it, the whole ladder is refused with `LADDER_LEVEL_REJECTED` and nothing is created or reserved.

The error names the level in its field violation: `field_path` is `trigger.strategy.ladder.levels[<index>]`, counted from zero, and `rule_id` is that level's own error code, such as `INSUFFICIENT_FUNDS`.

Each placed level is an ordinary GTC limit order with its own reservation, fills, and final state. Its client order ID is `trg:<trigger_id>:<child_seq>`, and Polyester records an `EVENT_FIRED` event for it with the level's price and order ID.

## How a ladder ends

A live ladder is `RUNNING`. `ladder_state.executed_qty_scaled` is the quantity its levels have filled, and `ladder_state.executed_levels` the number of levels with at least one fill. A partly filled level counts once.

Canceling the ladder ends it at once. Otherwise it ends only after every level has ended:

| Final status | When                                                                                                                                                                                                                       |
| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `COMPLETED`  | Any level filled, however the other levels ended. Also when nothing filled and only self-trade prevention, or Polyester while it recovered your account, canceled the levels                                               |
| `FAILED`     | The book rejected every level before it was placed, for example post-only levels that would cross. `failure_reason` is the first rejected level's                                                                          |
| `CANCELED`   | You canceled the ladder itself, or its Pair was disabled (`PAIR_DRAIN_LADDER`) or halted (`MARKET_HALTED`). Its open levels are canceled with it. Also `USER_REQUEST` when you canceled its levels and none of them filled |

> **Completed does not mean fully filled**
>
> A `COMPLETED` ladder may have filled only part of its quantity. Read `executed_qty_scaled`, `executed_levels`, and each level's order status for what filled.

Canceling a level, alone, with a cancel-all, or by the dead-man switch, does not cancel the ladder while other levels stay open. Once none remains open, a ladder that filled nothing ends `CANCELED` with `USER_REQUEST` and one `EVENT_CANCELED`; one that filled any quantity ends `COMPLETED`. Fills are never reversed: a canceled ladder keeps what its levels already filled.

A level that Polyester cancels while it recovers your account is not your cancel. Its order ends with cancel reason `ACCOUNT_REPAIR`, and a ladder whose levels end this way with nothing filled ends `COMPLETED` with nothing executed and no cancel reason. If the recovery ends the ladder itself, the ladder ends `CANCELED` with `ACCOUNT_REPAIR`.

## Managing a ladder

A ladder takes only [`CancelTrigger`](https://testnet.polyester.com/docs/api-docs/connect/triggers.v1.TriggersService/CancelTrigger). Pause, resume, and modify are refused with `TRIGGER_NOT_MODIFIABLE`. You can amend or cancel a level like any order, but not raise its quantity above the level's planned quantity.

## Limits and rate limits

An account can have up to 5 ladders on one Pair at once, and each counts toward its 100-trigger limit on that Pair. A ladder beyond either limit is refused with `MARKET_LADDER_LIMIT_EXCEEDED` or `MARKET_TRIGGER_LIMIT_EXCEEDED`.

Creating a ladder costs one unit of `TRADING_PLACE` plus one per level, charged together. If your placement budget cannot cover them all, the whole ladder is refused and nothing is charged for its levels. See [Rate limits](https://testnet.polyester.com/docs/developer-docs/shared-concepts/rate-limits).

## Example

A ladder that buys 0.01 BTC in five levels from 98,000 to 102,000. With a quantity step of 0.001 BTC or finer, it places 0.002 BTC at 98,000, 99,000, 100,000, 101,000, and 102,000.

## REST JSON

`POST /v1/triggers`

```json
{
	"trigger": {
		"symbol": "BTC-USDT",
		"qty": "0.01",
		"clientTriggerId": "ladder-btc-1",
		"ladder": {
			"side": "BUY",
			"priceMin": "98000",
			"priceMax": "102000",
			"levels": 5,
			"postOnly": true
		}
	}
}
```

## ConnectRPC ProtoJSON

`/triggers.v1.TriggersService/CreateTrigger`, assuming Pair ID `1` and a `base_quantity_scale` of 8.

```json
{
	"trigger": {
		"symbolId": 1,
		"qtyScaled": "1000000",
		"clientTriggerId": "ladder-btc-1",
		"ladder": {
			"side": "BUY",
			"priceMinTicks": "98000000000000",
			"priceMaxTicks": "102000000000000",
			"levels": 5,
			"postOnly": true
		}
	}
}
```

## Errors

| Stable code                    | Cause                                                                                                                       |
| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------- |
| `LADDER_LEVEL_REJECTED`        | One level cannot be placed. The field violation names the level and its own code                                            |
| `VALIDATION_ERROR`             | `levels` is outside `2` to `100`, the minimum is not below the maximum, or the range holds fewer ticks than the levels need |
| `PRICE_TICK_SIZE`              | The minimum or maximum price is not on the Pair's tick size                                                                 |
| `MARKET_LADDER_LIMIT_EXCEEDED` | The account already has 5 ladders on the Pair                                                                               |
| `TRIGGER_NOT_MODIFIABLE`       | A pause, resume, or modify was sent for a ladder                                                                            |

See [Error codes](https://testnet.polyester.com/docs/developer-docs/error-handling/error-codes) for HTTP and ConnectRPC statuses.
