Welcome to our new support center!
We provide 24x7 support by human agents! So if our documentation can’t help you out, feel free to reach out and create a ticket!

Altrady Support Altrady Support

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 price or pricePercentage on a target, never both. Same for stopPrice and stopPercentage, and for limitPrice and limitPercentage.
  • Set exitPriceType to percentage when the targets carry percentages.
  • Percentages are a whole ladder decision. Mixing priced and percentage targets in one orders array is not supported.
  • A stop-limit authored in percentages sends both stopPercentage and limitPercentage, 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:

  • pricePercentage on a take profit, stopPercentage or limitPercentage on a stop
  • exitPercentage, exitPriceType, keepEntriesOpen on the take profit ladder
  • priceScale or sizeScale on either ladder
  • trailingType or trailingPercentage on the stop
  • triggerType or candleResolution on 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.o is 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.
  • t is time. 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.

Was this article helpful?