Trade setup format v2 for partners
A trade setup is the JSON you send as the data field when creating a trade setup through the Altrady API. It describes one planned trade: how to enter, where to take profit, where to stop out, and when the plan expires. Altrady stores it, renders a card image from it, and can convert it into a signal. This article documents version 2 of that format, which is the version to write for any new integration.
What a trade setup is
Version 2 uses readable keys and named fields, and it is the only version that can express the things partners most often need, above all take profits and stops as percentages rather than fixed prices.
{
"version": 2,
"direction": "long",
"entry": {"type": "market"},
"takeProfit": {
"orders": [{"pricePercentage": 5, "positionPercentage": 100}],
"exitPriceType": "percentage"
},
"stopLoss": {"orderType": "market", "stopPercentage": 2}
}
That is a complete, valid setup: enter at market, take profit 5% away, stop 2% away. It works at any price because nothing in it is a fixed number tied to a moment.
There is an older version 1 with single-letter keys and positional tuples, still accepted and still common in stored setups. If you are reading setups you did not write, or porting an existing v1 integration, see the Migrating from v1 section at the end.
The payload
Only version, direction and entry are required. Every other section is optional and is left out when unused.
{
"version": 2,
"direction": "long",
"entry": {
"type": "ladder",
"orders": [
{"price": 38700, "positionPercentage": 60},
{"price": 38500, "positionPercentage": 40}
]
},
"takeProfit": {
"orders": [
{"price": 39500, "positionPercentage": 50},
{"price": 40200, "positionPercentage": 50}
],
"trailingDistance": 0.5
},
"stopLoss": {
"orderType": "limit",
"stopPrice": 37900,
"limitPrice": 37850,
"protection": "breakEven",
"riskPercentage": 1,
"coolDown": {"type": "time", "amount": 4, "timeFrame": "hour", "cancelPrice": 38100}
},
"entryCondition": {"startAt": 1798761600, "price": 38800, "operator": ">"},
"entryExpiration": {"expiresAt": 1798848000, "price": 38400},
"autoClose": {"amount": 6, "timeFrame": "hour"}
}
Top level
| Field | Values | Notes |
|---|---|---|
version |
2 |
Required. How readers tell the versions apart. |
direction |
long, short
|
Required. Decides the sign of every percentage. |
time |
Unix seconds | Creation time. Stamped by Altrady on save if absent. |
entry |
object | Required. See below. |
takeProfit |
object | Optional. |
stopLoss |
object | Optional. |
entryCondition |
object | Optional. Delays the entry until a price or time. |
entryExpiration |
object | Optional. Cancels the entry at a price or time. |
autoClose |
{amount, timeFrame} |
Optional. timeFrame is minute, hour or day. |
entry
type decides which of the remaining fields apply.
type |
Fields |
|---|---|
market |
none |
stopMarket |
stopPrice |
limit |
price |
stopLimit |
price, stopPrice
|
oco |
price, stopPrice, otherPrice
|
trailingStopLimit |
triggerPrice, stopOffset, limitOffset (offsets are percentages) |
ladder |
orders[{price, positionPercentage}], and optionally priceScale, sizeScale
|
positionPercentage values across a ladder are a split of the position and should total 100.
takeProfit
| Field | Values | Notes |
|---|---|---|
orders |
array of {price, positionPercentage} or {pricePercentage, positionPercentage}
|
Each target takes positionPercentage of the position. |
trailingDistance |
percentage | Trails the last target only. |
exitPercentage |
number | How much of the position the ladder exits. Defaults to 100. |
exitPriceType |
fixed, percentage
|
Set to percentage when targets carry pricePercentage. |
keepEntriesOpen |
boolean | Defaults to true. |
priceScale |
LINEAR, EXPONENTIAL, FIBONACCI, CUSTOM
|
Optional. |
sizeScale |
EQUAL, LINEAR, LINEAR_REVERSE, EXPONENTIAL, EXPONENTIAL_REVERSE, CUSTOM, REMAINDER
|
Optional. |
stopLoss
| Field | Values | Notes |
|---|---|---|
orderType |
limit, market
|
limitPrice and limitPercentage apply to limit only. |
stopPrice / stopPercentage
|
number | One or the other, not both. |
limitPrice / limitPercentage
|
number | One or the other, not both. |
protection |
price, followTakeProfit, breakEven
|
Omit for no protection. |
trailingPrice, trailingDistance
|
number | Used when protection is price. |
trailingType |
price, position
|
Optional. Ignored by signals today. |
trailingPercentage |
number | Optional, with trailingType: "position". |
riskPercentage |
number | Account risk the author sized the trade at. |
coolDown |
object |
{type: "time", amount, timeFrame} or {type: "candleClose", candleResolution}, plus optional cancelPrice. |
entryCondition and entryExpiration
Both take price and a time (startAt on the condition, expiresAt on the expiration), as Unix seconds. entryCondition also takes operator (> or <, or OR / AND when combining price and time), triggerType (price or candleClose) and candleResolution in minutes. entryExpiration has no operator.
Percentages instead of prices
A percentage-authored setup carries no prices at all. Targets and stops are distances from the entry, resolved when the setup is opened, so a setup written at 38,000 still makes sense when someone imports it at 84,000. This is the one thing v1 could not express, and it is why v2 exists for most partners.
{
"version": 2,
"direction": "long",
"entry": {"type": "market"},
"takeProfit": {
"orders": [
{"pricePercentage": 5, "positionPercentage": 50},
{"pricePercentage": 8, "positionPercentage": 50}
],
"exitPriceType": "percentage"
},
"stopLoss": {"orderType": "market", "stopPercentage": 2, "protection": "breakEven"}
}
Percentages are unsigned magnitudes. The side gives the sign. A take profit is always in the profit direction and a stop is always in the loss direction, so the same payload works for a long and a short with only direction changed. Never send a negative percentage.
| Direction | Take profit at 5% | Stop at 2% |
|---|---|---|
long |
entry + 5% | entry - 2% |
short |
entry - 5% | entry + 2% |
At an entry price of 84,000, the example above resolves to targets at 88,200 and 90,720 and a stop at 82,320 for a long, and to 79,800, 77,280 and 85,680 for a short.
Rules
- Send
priceorpricePercentageon a target, never both. Same forstopPriceandstopPercentage, and forlimitPriceandlimitPercentage. - Set
exitPriceTypetopercentagewhen the targets carry percentages. - Percentages are a whole ladder decision. Mixing priced and percentage targets in one
ordersarray is not supported. - A stop-limit authored in percentages sends both
stopPercentageandlimitPercentage, off the same basis. For a long the limit percentage is the larger of the two, because the limit sits below the trigger.
What resolves them. The basis is the average entry price of the setup, which for a market entry is the price at the moment it is opened. Altrady clients resolve against the same basis the stop loss interface shows percentages against, so the number the author typed is the number the recipient gets. Card images render against the market price at render time. Signals pass the percentages through untouched: the signal contract carries price_percentage and stop_percentage natively and resolves them when the position opens.
One gap worth knowing: limitPercentage is parsed and stored but nothing consumes it yet. Signals derive their own limit price from the stop plus slippage. This matches v1, where the limit price was never read either.
Creating a trade setup through the API
POST /trade_setups takes the payload as a JSON string in data. Nothing about the request changed for v2: the same endpoint and the same parameters accept either version, and the version is read from inside the string.
| Parameter | Type | Required | Notes |
|---|---|---|---|
coinray_symbol |
string | yes | The market, for example BINA_USDT_BTC. |
resolution |
string | yes | Chart resolution the setup was authored on. |
data |
string | yes | The v1 or v2 payload, JSON encoded as a string. |
title |
string | no | |
description |
string | no | |
is_public |
boolean | no | Defaults to false. Publishes a shareable item. |
skip_screenshot |
boolean | no | Defaults to true. |
data is a string, not a nested object. Serialize the payload first.
{
"coinray_symbol": "BINA_USDT_BTC",
"resolution": "60",
"title": "BTC long off the 4h range low",
"data": "{\"version\":2,\"direction\":\"long\",\"entry\":{\"type\":\"market\"},\"takeProfit\":{\"orders\":[{\"pricePercentage\":5,\"positionPercentage\":100}],\"exitPriceType\":\"percentage\"},\"stopLoss\":{\"orderType\":\"market\",\"stopPercentage\":2}}"
}
The response returns the stored setup, including its external_id and share URL. Read it back with GET /trade_setups/:external_id, list your recent ones with GET /trade_setups, and remove one with DELETE /trade_setups/:external_id.
What Altrady does with data. It is validated as JSON and stamped with a creation time if it has none. From there it feeds three things: the card image rendered after creation and on the public share page, the import into the trading terminal when someone opens the setup, and conversion into a signal when the setup is created with a signal provider. All three read v1 and v2.
If you read setups rather than write them, the data field is returned exactly as stored, so a consumer must handle both versions. The simplest approach is the one Altrady uses internally: normalize v1 to v2 on read using the mapping in Migrating from v1 below, then write the rest of your code against v2 only.
Compatibility rules
Detect the version before parsing. version: 2 or v: 1 is the only reliable signal. Do not infer it from the presence of a key.
const version = payload.version || payload.v
Accept both, always. Setups created years ago are still v1 and are never rewritten. Anything you read may be either version.
Write v2. It is the current format and the only one that carries percentages, ladder scales, trailing type or candle close triggers. Altrady clients write v1 only when a setup happens to fit in v1, which keeps older readers working for the setups they can understand, but a new integration has no reason to emit v1 at all.
The fields v1 cannot carry, so a setup using any of them is always v2:
-
pricePercentageon a take profit,stopPercentageorlimitPercentageon a stop -
exitPercentage,exitPriceType,keepEntriesOpenon the take profit ladder -
priceScaleorsizeScaleon either ladder -
trailingTypeortrailingPercentageon the stop -
triggerTypeorcandleResolutionon the entry condition
Reject unknown versions rather than guessing. A reader that treats an unknown version as v1 produces a plausible but wrong trade, which is worse than an error.
Known gaps
| Field | Status |
|---|---|
limitPercentage |
Parsed and stored, not yet consumed. Signals derive a limit from the stop plus slippage. |
trailingType, trailingPercentage
|
Accepted and round-tripped, ignored by signals and card rendering. |
ex.o |
Removed in v2. Was never written or read in v1. |
Rollout
v2 support is built on both the Altrady backend and the desktop client, and the two release together. Until both are live, a percentage-authored setup cannot be rendered or converted into a signal, so hold those until the release lands. Everything else in v2 is safe to send today.
Migrating from v1
The mapping is mechanical and lossless in both directions, with two exceptions noted at the end. If you already have a v1 writer, the shortest path is a translation function from your v1 shape to v2, keeping the rest of your code unchanged.
Top level
| v1 | v2 | Notes |
|---|---|---|
v: 1 |
version: 2 |
|
d: "L" / "S"
|
direction: "long" / "short"
|
|
t |
time |
Unix seconds, both versions. |
e |
entry |
|
tp |
takeProfit |
|
sl |
stopLoss |
|
ec |
entryCondition |
|
ex |
entryExpiration |
|
ac: "6h" |
autoClose: {amount: 6, timeFrame: "hour"} |
Suffix m, h, d becomes minute, hour, day. |
Entry
e.t becomes entry.type, and the positional e.o becomes named fields.
v1 e.t
|
v2 entry.type
|
v1 e.o becomes |
|---|---|---|
M |
market |
nothing |
S |
stopMarket |
stopPrice |
L |
limit |
price |
SL |
stopLimit |
[price, stopPrice] |
OCO |
oco |
[price, stopPrice, otherPrice] |
TSL |
trailingStopLimit |
[triggerPrice, stopOffset, limitOffset] |
LL |
ladder |
[[price, positionPercentage], ...] becomes orders[{price, positionPercentage}]
|
Take profit and stop loss
| v1 | v2 |
|---|---|
tp.o: [[price, pct], ...] |
takeProfit.orders: [{price, positionPercentage}, ...] |
tp.td |
takeProfit.trailingDistance |
sl.t: "SL" / "SM"
|
stopLoss.orderType: "limit" / "market"
|
sl.o: [stopPrice, limitPrice] |
stopLoss.stopPrice, stopLoss.limitPrice
|
sl.p: "P" / "FTP" / "BE"
|
stopLoss.protection: "price" / "followTakeProfit" / "breakEven"
|
sl.tp, sl.td
|
stopLoss.trailingPrice, stopLoss.trailingDistance
|
sl.r |
stopLoss.riskPercentage |
sl.cd: {t: "4h"} |
stopLoss.coolDown: {type: "time", amount: 4, timeFrame: "hour"} |
sl.cd: {cc: 60} |
stopLoss.coolDown: {type: "candleClose", candleResolution: 60} |
sl.cd.cdc |
stopLoss.coolDown.cancelPrice |
Conditions
ec.t becomes entryCondition.startAt, ex.t becomes entryExpiration.expiresAt, and p and o become price and operator on both.
Two deliberate differences
-
ex.ois dropped. v1 readers looked for an operator on the expiration, but no writer ever sent one and an expiration has no operator to apply. If your v1 payloads contain it, drop it. Nothing consumed it. -
tistime. Altrady stamps a creation time on save if you leave it out, using the key that matches the version you sent.
Worked example
The same setup in both versions. A long, two entry rungs at 60/40, two targets at 50/50 with trailing on the last, a stop-limit with break-even protection and a four-hour cool down.
{"v":1,"d":"L",
"e":{"t":"LL","o":[[38700,60],[38500,40]]},
"tp":{"o":[[39500,50],[40200,50]],"td":0.5},
"sl":{"t":"SL","o":[37900,37850],"p":"BE","r":1,"cd":{"t":"4h","cdc":38100}},
"ec":{"t":1798761600,"p":38800,"o":">"},
"ac":"6h"}
{"version":2,"direction":"long",
"entry":{"type":"ladder","orders":[{"price":38700,"positionPercentage":60},{"price":38500,"positionPercentage":40}]},
"takeProfit":{"orders":[{"price":39500,"positionPercentage":50},{"price":40200,"positionPercentage":50}],"trailingDistance":0.5},
"stopLoss":{"orderType":"limit","stopPrice":37900,"limitPrice":37850,"protection":"breakEven","riskPercentage":1,
"coolDown":{"type":"time","amount":4,"timeFrame":"hour","cancelPrice":38100}},
"entryCondition":{"startAt":1798761600,"price":38800,"operator":">"},
"autoClose":{"amount":6,"timeFrame":"hour"}}
Still stuck?
If a payload is rejected or renders differently from what you sent, contact us through the support chat with the exact data string and we will check it with you.