# TWAP

Split a quantity into market or limit slices placed at a fixed interval, and know how slices are sized, funded, and how a TWAP ends.

A TWAP (time-weighted average price) trigger splits a total quantity into slices and places one slice per interval over a duration. Use it to work a larger order without sending it to the book at once. A TWAP does not watch a price and does not guarantee the market's average price or a full fill. Create one 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 `twap` strategy.

## Fields

| Field                    | Rule                                                                                                                              |
| ------------------------ | --------------------------------------------------------------------------------------------------------------------------------- |
| `qty_scaled`             | Required, positive. The total quantity of all slices                                                                              |
| `twap.side`              | Required: `BUY` or `SELL`                                                                                                         |
| `twap.duration_ms`       | Required, at least `1000` (1 second)                                                                                              |
| `twap.slice_interval_ms` | Required, at least `100` and at most `duration_ms`                                                                                |
| `twap.market_ioc`        | Market slices. Optional `max_slippage_ticks` or `max_slippage_bps` (`1` to `10000` for a buy, `1` to `9999` for a sell) per slice |
| `twap.limit_gtc`         | Limit slices at `price_ticks`, on the Pair's tick size. Takes no slippage cap                                                     |

Set exactly one of `market_ioc` and `limit_gtc`. `fee_asset`, `self_trade_prevention_mode`, and `client_trigger_id` follow the same rules as other triggers. In REST, the limit price is `limitGtc.price` and the quantity `qty`, both decimal strings; the durations stay integers.

## How slices are planned and sized

The number of slices is `duration_ms` divided by `slice_interval_ms`, rounded up, and at most 1,000,000. The first slice is placed when the TWAP is accepted, and each later slice one interval after the previous one.

Each slice's quantity is decided when it is due: the quantity not yet filled or open, divided by the slices left, rounded down to the Pair's quantity step. Quantity that an earlier slice did not fill moves into later slices, and the last slice takes everything that remains.

Because slices never shrink while they fill, the first slice is the smallest. A create whose first slice could never pass the Pair's order limits is rejected with that check's code, and nothing is created: `STEP_SIZE` when it rounds down to zero, or `MIN_QUANTITY` or `MIN_NOTIONAL`. A market slice is valued at its slippage bound from the current price.

### Example: 1 BTC over 10 minutes

A TWAP buys 1 BTC with `duration_ms` 600000 and `slice_interval_ms` 60000, so it plans 10 slices. The Pair's quantity step is 0.0001 BTC in this example.

1. The first slice buys 0.1 BTC and fills 0.1.
2. The second slice buys 0.1 BTC and fills only 0.04. The remaining 0.86 BTC is spread over the 8 slices left.
3. The third slice buys 0.1075 BTC, and so on, until the tenth slice places what is left.

## Market and limit slices

| Slices       | Behaviour                                                                                                                                                                                              |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `market_ioc` | Each slice takes liquidity at once, within its slippage cap of the reference price when it is placed. Without a cap, the Pair's default market slippage applies. Any unfilled part is canceled at once |
| `limit_gtc`  | Each slice rests at your limit price. When the next slice is due, the previous one is canceled first, so only one slice rests at a time                                                                |

The slippage cap protects each slice on its own. It does not bound how far the price moves over the whole duration. See [Market order price protection](https://testnet.polyester.com/docs/developer-docs/shared-concepts/market-order-price-protection).

## Funds

A TWAP reserves nothing when it is created. Each slice is funded when it is placed, like an order you send. A slice that your available balance cannot cover is rejected, and the TWAP fails.

## How a TWAP ends

A live TWAP is `RUNNING`. `twap_state` shows `slice_idx`, `slice_count`, and `executed_qty_scaled`, the quantity its slices have filled.

| Final status | When                                                                                                                                |
| ------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
| `COMPLETED`  | Every slice was placed and none is still open, or the slices filled the total quantity                                              |
| `FAILED`     | A slice was rejected. `failure_reason` is the slice's reason, such as `INSUFFICIENT_FUNDS`                                          |
| `CANCELED`   | You canceled the TWAP, or its Pair was disabled (`PAIR_DRAIN_TWAP`) or halted (`MARKET_HALTED`). Its open slice is canceled with it |

> **Completed does not mean fully filled**
>
> Market slices that find no liquidity within their cap, or limit slices canceled before they fill, leave quantity unfilled. Compare `executed_qty_scaled` with the total quantity.

Slices placed before a TWAP fails or is canceled remain real orders, and their fills stand. A failed TWAP does not retry the rejected slice and places no further slices.

The duration is a plan, not a hard deadline. A slice that waits, for example for placement budget or for the previous limit slice to be canceled, delays the slices after it.

## Managing a TWAP

A TWAP 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`; cancel and create a new TWAP to change it.

Each slice is an ordinary order with the client order ID `trg:<trigger_id>:<child_seq>`, and `EVENT_FIRED` names it. You can amend or cancel a slice like any order, but you cannot raise it above what the TWAP still has to execute: that is refused with `CHILD_QTY_EXCEEDS_REMAINING`. Canceling a slice does not cancel the TWAP. A cancel-all cancels the open slice and leaves the TWAP running.

## Limits

An account can run up to 20 TWAPs on one Pair at once, and they count toward its 100-trigger limit on that Pair. A TWAP beyond either limit is refused with `MARKET_TWAP_LIMIT_EXCEEDED` or `MARKET_TRIGGER_LIMIT_EXCEEDED`, and nothing is created. When the Pair has no room for another TWAP across all accounts, the create is refused with `TWAP_CAPACITY_REACHED`; retry later.

Creating a TWAP costs one unit of `TRADING_PLACE`, and each slice costs one more when it is placed. A slice that meets an empty budget, or an account under an automatic repair, waits and is placed shortly; the TWAP records `EVENT_FIRE_REFUSED` for the refused attempt and stays `RUNNING`. See [Rate limits](https://testnet.polyester.com/docs/developer-docs/shared-concepts/rate-limits).

## Example

A TWAP that buys 1 BTC over 10 minutes in market slices, each capped at 0.5% slippage.

## REST JSON

`POST /v1/triggers`

```json
{
	"trigger": {
		"symbol": "BTC-USDT",
		"qty": "1",
		"clientTriggerId": "twap-btc-1",
		"twap": {
			"side": "BUY",
			"durationMs": 600000,
			"sliceIntervalMs": 60000,
			"marketIoc": { "maxSlippageBps": 50 }
		}
	}
}
```

## ConnectRPC ProtoJSON

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

```json
{
	"trigger": {
		"symbolId": 1,
		"qtyScaled": "100000000",
		"clientTriggerId": "twap-btc-1",
		"twap": {
			"side": "BUY",
			"durationMs": "600000",
			"sliceIntervalMs": "60000",
			"marketIoc": { "maxSlippageBps": 50 }
		}
	}
}
```

## Errors

| Stable code                                 | Cause                                                            |
| ------------------------------------------- | ---------------------------------------------------------------- |
| `TWAP_DURATION_INVALID`                     | `duration_ms` is below `1000`                                    |
| `TWAP_INTERVAL_INVALID`                     | `slice_interval_ms` is below `100` or above `duration_ms`        |
| `MAX_SLIPPAGE_INVALID`                      | A sell's slice cap is `10000` bps                                |
| `PRICE_TICK_SIZE`                           | The limit price is not on the Pair's tick size                   |
| `MARKET_TWAP_LIMIT_EXCEEDED`                | The account already runs 20 TWAPs on the Pair                    |
| `STEP_SIZE`, `MIN_QUANTITY`, `MIN_NOTIONAL` | The first slice could never pass the Pair's order limits         |
| `TRIGGER_NOT_MODIFIABLE`                    | A pause, resume, or modify was sent for a TWAP                   |
| `CHILD_QTY_EXCEEDS_REMAINING`               | A slice edit would leave more than the TWAP's remaining quantity |

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