Time-Weighted Average Price
Spread a basket entry into timed chunks.
A TWAP schedule splits the basket into evenly spaced slices over a window, which reduces market impact compared with one market order.
Create one with POST /schedules (SDK: sdk.core.schedules.create).
Open over time
{
"strategy": "TWAP",
"intent": "OPEN",
"executionType": "MARKET",
"startAt": "2026-01-01T00:00:00.000Z",
"endAt": "2026-01-01T01:00:00.000Z",
"sliceIntervalSec": 300,
"targetNotional": "5000",
"legs": [
{ "symbol": "0", "side": "BUY", "weight": 0.5 },
{ "symbol": "1", "side": "SELL", "weight": 0.5 }
],
"clientId": "your-client-id"
}That works 5,000 USD into a long BTC / short ETH basket over one hour, in a slice every five minutes.
A TWAP sizes legs differently from a market order. You give one targetNotional for the whole basket, and each leg takes a weight of it. Leg weights must sum to exactly 1. A market order sizes each leg on its own, with mode and amount.
| Field | Rule |
|---|---|
startAt, endAt | ISO-8601 timestamps. startAt must be before endAt, and the window must be at least 15 seconds. |
sliceIntervalSec | Integer, 15 seconds to 31 days (2,678,400 seconds). |
targetNotional | Unsigned decimal string, greater than zero. |
legs[].weight | Share of targetNotional. All weights sum to exactly 1. |
How many slices you get
sliceIntervalSec alone does not decide the slice count. The last 15 seconds of the window are reserved as delivery headroom for the final slice, and the first slice starts at startAt:
sliceCount = max(1, floor((windowSeconds - 15) / sliceIntervalSec) + 1)The example above is a one-hour window at 300-second slices, so it runs 12 slices, not 13. An interval longer than the window collapses to a single slice.
The response echoes the computed sliceCount, so you do not have to derive it yourself.
Close over time
Set intent: "CLOSE" and give a positionId instead of legs and a notional. The schedule unwinds the whole position across the window:
{
"strategy": "TWAP",
"intent": "CLOSE",
"executionType": "MARKET",
"positionId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"startAt": "2026-01-01T00:00:00.000Z",
"endAt": "2026-01-01T01:00:00.000Z",
"sliceIntervalSec": 300
}Hide the pattern
Evenly spaced, equally sized slices are easy for others to read. randomization jitters each slice, in basis points of the interval and of the slice size:
{ "randomization": { "timingJitterBps": 500, "sizeJitterBps": 300 } }Both fields accept integers from 0 to 5000 bps, and both default to 0. randomization works on CLOSE schedules as well as OPEN ones.
What gets rejected
Two rules reject a schedule at creation time, on both OPEN and CLOSE. Neither is visible from the field tables above.
| Rule | Status |
|---|---|
One active schedule per position key. A second POST /schedules for the same instruments on the same trade account is refused while the first is still active, whether it opens or closes. Cancel the first, or wait for it to finish. | 409 |
Slices must clear the venue minimum. Pear shrinks a slice by the largest downward sizeJitterBps swing and checks the result against every leg's minimum order notional. If a slice could fall below it, the whole plan is refused rather than stalling part-way through the window. Fix it with fewer slices, less size jitter, or a larger targetNotional. | 400 |
Response
200 OK, not 202. The body carries the created schedule:
{
"schedule": {
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"connector": "hyperliquid",
"strategy": "TWAP",
"status": "ACTIVE",
"intent": "OPEN",
"positionKey": "9f2a6c1b4e07d38a5c91f0b2d47e63a8c05b9142ef7d3608ba1c4f52d9e07a63",
"executionType": "MARKET",
"startAt": "2026-01-01T00:00:00.000Z",
"endAt": "2026-01-01T01:00:00.000Z",
"sliceIntervalSec": 300,
"sliceCount": 12,
"randomization": { "timingJitterBps": 0, "sizeJitterBps": 0 },
"legs": [
{ "symbol": "0", "side": "BUY", "weight": 0.5 },
{ "symbol": "1", "side": "SELL", "weight": 0.5 }
],
"targetNotional": "5000",
"remainingNotional": "5000",
"createdAt": "2026-01-01T00:00:00.000Z",
"updatedAt": "2026-01-01T00:00:00.000Z"
}
}| Field | Meaning |
|---|---|
id | Schedule UUID. Cancel the schedule with it. |
status | ACTIVE on creation, then COMPLETED, FAILED, or CANCELLED. |
sliceCount | How many slices the window holds. Computed by Pear, never sent by you. |
remainingNotional | Starts equal to targetNotional and falls as slices fill. |
positionKey | The identity of the instrument set this schedule builds. The one-active-schedule rule is keyed on it. See Position Key. |
Manage schedules
| Action | Endpoint |
|---|---|
| List | GET /schedules |
| Cancel one | PATCH /schedules/{scheduleId}/cancel |
| Cancel all | PATCH /schedules/cancel-all |
Cancelling stops the pending slices. Slices that already filled stay as part of the position. Progress streams over the WebSocket as schedule.created, a per-slice execution, then schedule.completed.