Take Profit & Multi-Target Ladders
Where stop-loss is mostly a single field, take-profit is an array. You can attach one target or twelve, percent or absolute, with optional per-target trailing. Six top-level TP fields plus the 5-property LimitOrder sub-shape.
- TPs go in the
targetsarray. Each entry is aLimitOrderwithamountplus eithertakeProfitPercentorprice. targetType: "percent"(default) → usetakeProfitPercent.targetType: "absolute"→ useprice.targetAmountInPercent: truemakes eachamounta percent share of the position. With it set, target amounts should sum to 100.takeProfitExpressionupdates onlytargets[0].priceand therefore belongs totargetType: "absolute"payloads.- Bybit-only flags:
setTpToPositionandtargetAssignedToPosition. They control whether TPs attach to the position vs sit as standalone reduce-only orders.
targets[].price (absolute mode), targets[].takeProfitPercent (percent mode), and targets[].amount accept Pine placeholders, as does takeProfitExpression. See Anatomy → Dynamic values via TradingView placeholders.
Quick reference table
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
takeProfitExpression | string | no | — | Arithmetic expression evaluated to produce the first target's absolute price (e.g. "{{strategy.order.price}}+0.02*{{strategy.order.price}}"). Operator sign auto-flips based on orderType. |
targets | List<LimitOrder> | no | — | Array of take-profit targets. Each entry is a LimitOrder with amount plus either price (absolute targets) or takeProfitPercent (percent targets), and optional trailing fields. Order matters — fills are processed in array order. |
targetType | string | no | percent | Selects the target value field. percent = use targets[].takeProfitPercent; absolute = use explicit targets[].price levels (supports takeProfitExpression). |
targetAmountInPercent | bool | no | false | When true, each target's amount is a percent share of the position. When false, amount is an absolute quantity in base units. |
targetAssignedToPosition | bool | no | false | Bybit-only. When targets contains exactly one entry, attach it to the position as a TP-on-position instead of a standalone reduce-only order. |
setTpToPosition | bool | no | false | Bybit-specific. Attach take-profit orders to the position rather than as separate reduce-only limits. Required when entering via limit orders on Bybit's UTA. |
LimitOrder fields (used inside targets[])
These properties only appear inside an entry of the targets array. They are not top-level TradeCommand fields.
| Property | Type | Required | Default | Description |
|---|---|---|---|---|
idx | int | no | 0 | Position of the target inside the ladder. Used by the UI / activity log to label fills (TP1, TP2, ...). Zero-based. |
price | decimal | yes | 0 | Absolute target price. Use when the parent TradeCommand has targetType=absolute. For targetType=percent, use takeProfitPercent instead. |
amount | decimal | yes | 0 | Size assigned to this target. Percent of position when targetAmountInPercent=true, otherwise absolute base-asset quantity. |
takeProfitPercent | decimal | no | 0 | Target distance in percent from entry. Use when the parent TradeCommand has targetType=percent. For targetType=absolute, use price instead. |
trailingPercent | decimal | no | 0 | Per-target trailing offset. Once price reaches this target, the trailing logic for that slice arms with this distance. |
The LimitOrder sub-shape
Each entry in targets is a LimitOrder object with five fields:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
idx | int | no | 0 | Zero-based position in the ladder. Used by the UI to label fills (TP1, TP2, …). TVH does not enforce uniqueness — convention is 0, 1, 2, … |
price | decimal | conditional | 0 | Absolute target price. Use when targetType="absolute". |
amount | decimal | yes | 0 | Size of this target. Percent of position when targetAmountInPercent=true, otherwise absolute base-asset quantity. |
takeProfitPercent | decimal | conditional | 0 | Percent distance from entry. Use when targetType="percent". |
trailingPercent | decimal | no | 0 | Per-target trailing offset. Once price reaches this target, the trailing logic for that slice arms with this distance. |
In JSON:
{
"targetType": "percent",
"targets": [
{ "idx": 0, "takeProfitPercent": 1.0, "amount": 50 },
{ "idx": 1, "takeProfitPercent": 2.0, "amount": 30 },
{ "idx": 2, "takeProfitPercent": 3.0, "amount": 20 }
]
}
Order matters. Fills are processed in array order: targets[0] is "TP1", targets[1] is "TP2", and so on.
To lock the stop at break-even once price reaches your TP level, use the trailing-stop companion stopLossToBreakEven and set the trailing activation threshold to that level. See Stop Loss → Trailing break-even. There is no automatic "move to break-even after TP1 fills".
Percent vs absolute target prices
Percent mode (default)
{
"targetType": "percent",
"targets": [
{
"takeProfitPercent": 0.8,
"amount": 50
},
{
"takeProfitPercent": 1.6,
"amount": 50
}
],
"targetAmountInPercent": true
}
Targets at +0.8% and +1.6% from entry on a long. Direction flips for shorts.
Absolute mode
{
"targetType": "absolute",
"targets": [
{ "price": 66500, "amount": 50 },
{ "price": 68000, "amount": 50 }
],
"targetAmountInPercent": true
}
Fixed price levels. Your job to ensure they're on the correct side of entry — TVH does not validate.
Absolute mode with expression (first target only)
{
"targetType": "absolute",
"takeProfitExpression": "{{strategy.order.price}}+0.02*{{strategy.order.price}}",
"targets": [
{ "price": 0, "amount": 100 }
]
}
Server evaluates the expression and writes the result to targets[0].price. For multi-TP ladders, set explicit prices on targets[1..n] — only the first is updated by the expression.
Sign-flip rules: + becomes - on a sell, - becomes + on a buy. Same engine as stopLossExpression. See Math Expressions.
Multi-TP scale-out
Three targets, equal share, percent-spaced:
{
"targetType": "percent",
"targetAmountInPercent": true,
"targets": [
{
"takeProfitPercent": 0.7,
"amount": 33
},
{
"takeProfitPercent": 1.4,
"amount": 33
},
{
"takeProfitPercent": 2.1,
"amount": 34
}
]
}
When the first target fills, 33% of the position closes. Two-thirds remain. When the second fills, another 33% closes. And so on.
Amount summation rule. When targetAmountInPercent=true, the sum of amount should equal 100. TVH does not normalise — sending [50, 30] (sum 80) leaves 20% of the position un-targeted. Sending [50, 60] (sum 110) tries to close more than 100% and the last target will be partially or fully rejected.
Adjust your Pine logic to always hit 100, or build the targets so the final slice picks up the rounding remainder (the 34 in the example above).
Trailing per target
Each target can carry its own trailing offset that arms once that level fills.
{
"targetType": "percent",
"targets": [
{ "takeProfitPercent": 0.8, "amount": 30 },
{ "takeProfitPercent": 1.6, "amount": 30 },
{ "takeProfitPercent": 0, "amount": 40, "trailingPercent": 0.5 }
]
}
The first two fills are flat partial closes at +0.8% and +1.6%. The third "target" has takeProfitPercent: 0 and trailingPercent: 0.5 — it isn't an execution target, it's a "trail the rest of the position at 0.5% offset" instruction. The trailing slice arms after the second target fills.
This mirrors the Trade Command Builder UI's Scale out then trail preset. Available on every exchange that supports trailing.
Bybit-only TP attachment
Bybit UTA allows two different TP delivery models:
Model A — standalone reduce-only limits (default)
Each target becomes a separate reduce-only limit order on the order book. Works on every exchange. Default behaviour.
Model B — TP attached to the position
{
"exchange": "bybit",
"targetType": "percent",
"setTpToPosition": true,
"targets": [
{ "takeProfitPercent": 0.8, "amount": 100 }
]
}
The single target attaches directly to the position rather than living as a standalone order. Required when entering via limit orders on Bybit UTA because the position doesn't exist yet at the moment the entry limit is placed — TVH must wait, then attach.
targetAssignedToPosition is the single-target shortcut: when targets contains exactly one entry and you want it on the position, set this instead of setTpToPosition. They differ in subtle Bybit-API details — prefer setTpToPosition unless you have a reason otherwise.
Both flags only apply to Bybit. Setting them on Binance, OKX, KuCoin, Coinbase, or BitMEX is silently ignored.
Detail per property
targets
| Attribute | Value |
|---|---|
| Type | List<LimitOrder> |
| Required | no |
| Default | [] |
| TV placeholder compatible | only inner fields (price, takeProfitPercent, amount) |
Array of take-profit targets. Each entry has the LimitOrder shape documented above.
targetType
| Attribute | Value |
|---|---|
| Type | string |
| Required | no |
| Allowed values | "percent", "absolute" |
| Default | "percent" |
Selects the target value field: percent uses targets[].takeProfitPercent; absolute uses targets[].price.
takeProfitExpression
| Attribute | Value |
|---|---|
| Type | string |
| Required | no |
| Default | "" |
| Auto-mapped | yes — sign-flipped by orderType, evaluated server-side |
| TV placeholder compatible | yes |
Arithmetic expression that resolves to an absolute price for targets[0]. Only evaluated when targetType="absolute" and at least one target exists in the array.
For multi-target ladders in absolute mode, set explicit price values on targets [1..n]. The expression updates only the first slot.
targetAmountInPercent
| Attribute | Value |
|---|---|
| Type | bool |
| Required | no |
| Default | false |
When true, each target's amount is a percent share of the position. When false, amount is an absolute base-asset quantity.
The Trade Command Builder UI defaults to true because percent-of-position is the more common pattern.
setTpToPosition
| Attribute | Value |
|---|---|
| Type | bool |
| Required | no |
| Default | false |
Bybit-only. Attach take-profit orders to the position rather than as separate reduce-only limits. Required when entering via limit orders on Bybit UTA.
targetAssignedToPosition
| Attribute | Value |
|---|---|
| Type | bool |
| Required | no |
| Default | false |
Bybit-only. Single-target shortcut: when targets has exactly one entry, attach it to the position. Prefer setTpToPosition for new payloads.
Per-exchange TP limits
| Exchange | Native multi-TP | Max targets | Notes |
|---|---|---|---|
| Binance Futures USDT-M | yes (reduce-only limits) | unlimited within order count cap | Counts against the open-orders-per-pair limit. |
| Binance Futures COIN-M | yes | unlimited within cap | Same as USDT-M. |
| Binance Spot | yes (one OCO at a time per pair) | 1 per OCO | TVH emulates multi-TP by chaining OCO orders. |
| Bybit UTA | yes | unlimited (standalone) / 1 (attached) | setTpToPosition limits to 1 TP on the position. |
| OKX | yes (algo orders) | unlimited within cap | Hedge mode needs positionSide. |
| KuCoin Futures | yes | unlimited within cap | |
| KuCoin Spot | partial (stop-limit only) | 1 | |
| Coinbase Perps | yes (algo) | unlimited within cap | |
| Coinbase Spot | emulated | 1 | TVH polls and submits. |
| BitMEX | yes (algo) | unlimited within cap |
Worked examples
Two-target scale-out with trailing break-even
{
"exchange": "binance-futures",
"pair": "BTCUSDT",
"apiKey": "binance-futures-main",
"isBuy": true,
"isMarket": true,
"leverage": 5,
"unitsType": "percent",
"unitsPercent": 3,
"stopLossType": "percent",
"stopLossPercent": 1.5,
"useTrailingStopLoss": true,
"trailingStopLossActivationPercent": 1.0,
"stopLossToBreakEven": true,
"targetType": "percent",
"targetAmountInPercent": true,
"targets": [
{
"takeProfitPercent": 1.0,
"amount": 50
},
{
"takeProfitPercent": 2.0,
"amount": 50
}
],
"token": "<token>"
}
3% of balance at 5x leverage, SL at −1.5%, two TPs at +1% and +2% (half the position each). With trailing on and the activation threshold at +1%, the stop locks at break-even once price reaches +1%, the level of TP1. Binance Futures and Bybit only.
Single TP attached to Bybit UTA position
{
"exchange": "bybit",
"pair": "BTCUSDT",
"apiKey": "bybit-uta-main",
"isBuy": true,
"isLimit": true,
"price": 63800,
"chaseLimitOrder": true,
"units": 0.05,
"stopLossType": "percent",
"stopLossPercent": 1.0,
"targetType": "percent",
"targets": [
{
"takeProfitPercent": 1.5,
"amount": 100
}
],
"setTpToPosition": true,
"token": "<token>"
}
Limit-chase entry, single TP at +1.5%, attached to the position so it survives the limit-order chase.
Common mistakes
- Forgetting
targetAmountInPercent. Without it,amount: 50is interpreted as 50 base-asset units, not 50% of the position. On a 0.05 BTC position that's a 1000× over-sell. TVH does not auto-detect — set the flag explicitly when you mean percent. takeProfitExpressionon a multi-TP ladder. It only updatestargets[0].pricein absolute mode. Targets[1..n]keep whatever absolutepriceyou sent. Either set explicit prices everywhere or use a single-target payload.- Bybit
setTpToPosition: truewithtargets.length > 1. Only the first target attaches; the rest are dropped. Bybit's position-side TP allows one TP value. - Sum of amounts ≠ 100. Easy to hit when the strategy generates fractional sizes. Always reconcile at the last entry.
Next
Futures Options — leverage, margin mode, hedge mode, the postOnly default trap.