{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://www.tv-hub.org/schemas/trade-command.schema.json",
  "title": "TradeCommand",
  "description": "TradeCommand payload sent to the TV-Hub webhook. Descriptions are placeholders until enriched from docs-site/data/_overrides/*.yaml.",
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "exchange": {
      "description": "Target exchange identifier. Determines which exchange receives the trade. Lowercase, hyphenated.",
      "type": "string",
      "enum": [
        "binance",
        "binance-futures",
        "binance-futures-testnet",
        "binance-futures-coin",
        "binance-futures-coin-testnet",
        "bybit",
        "bybit-testnet",
        "bybit-spot",
        "bybit-spot-testnet",
        "okx",
        "kucoin",
        "kucoin-spot",
        "coinbase",
        "coinbase-spot",
        "bitmex",
        "bitmex-testnet"
      ],
      "examples": [
        "binance-futures"
      ]
    },
    "pair": {
      "description": "Trading pair in exchange-native format. Each exchange has its own naming rules (e.g. `BTCUSDT` for Binance, `BTC-USDT-SWAP` for OKX, `XBTUSD` for BitMEX).",
      "type": "string",
      "examples": [
        "BTCUSDT"
      ]
    },
    "isBuy": {
      "description": "Marks the signal as a long entry. Use this for normal webhook/manual TradeCommands. Mutually exclusive with `isSell` and `isClose`.",
      "type": "boolean",
      "examples": [
        "true"
      ]
    },
    "isSell": {
      "description": "Marks the signal as a short/ sell entry. Use this for normal webhook/manual TradeCommands. Mutually exclusive with `isBuy` and `isClose`.",
      "type": "boolean",
      "examples": [
        "true"
      ]
    },
    "isMarket": {
      "description": "Sends a market order. When `false` (and `isLimit` is true) TVH uses the `price` field as a limit price.",
      "type": "boolean",
      "examples": [
        "true"
      ]
    },
    "isClose": {
      "description": "Marks the signal as a close-only action. Combined with `closeLong` / `closeShort` for hedge-mode flat commands.",
      "type": "boolean",
      "examples": [
        "true"
      ]
    },
    "isLimit": {
      "description": "Sends a limit order at `price`. Ignored when `isMarket` is true.",
      "type": "boolean",
      "examples": [
        "true"
      ]
    },
    "isStopLoss": {
      "description": "Marks the signal as a standalone stop-loss order (not an entry with attached SL). Used to add an SL to an existing position.",
      "type": "boolean",
      "examples": [
        "true"
      ]
    },
    "price": {
      "description": "Limit price for non-market orders. Also the reference price for absolute stop-loss / take-profit calculations.",
      "type": "number",
      "examples": [
        "65250.5"
      ]
    },
    "units": {
      "description": "Position size. Interpretation depends on `unitsType` (absolute base units, percent of balance, or risk-based).",
      "type": "number",
      "examples": [
        "0.05"
      ]
    },
    "stopLoss": {
      "description": "Absolute stop-loss price. Required when `stopLossType=\"absolute\"`. Used as the reference for break-even and trailing offsets.",
      "type": "number",
      "examples": [
        "63800"
      ]
    },
    "stopLossPercent": {
      "description": "Stop-loss distance in percent from entry. Required when `stopLossType=\"percent\"`.",
      "type": "number",
      "examples": [
        "1.5"
      ]
    },
    "stopLossType": {
      "description": "How to interpret stop-loss input. `absolute` = price level (or `stopLossExpression`), `percent` = distance from entry.",
      "type": "string",
      "enum": [
        "absolute",
        "percent"
      ],
      "examples": [
        "percent"
      ]
    },
    "stopLossExpression": {
      "description": "Arithmetic expression evaluated by TVH to produce an absolute stop price (e.g. `\"3660-0.02*3660\"`). Supports Pine placeholders like `{{strategy.order.price}}`. Operator sign is auto-flipped based on `orderType` so the SL lands on the correct side.",
      "type": "string",
      "examples": [
        "{{strategy.order.price}}-0.02*{{strategy.order.price}}"
      ]
    },
    "takeProfitExpression": {
      "description": "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`.",
      "type": "string",
      "examples": [
        "{{strategy.order.price}}+0.02*{{strategy.order.price}}"
      ]
    },
    "token": {
      "description": "Your TVH User Token, a GUID that authenticates the webhook caller. Treat as a password.",
      "type": "string",
      "examples": [
        "YOUR_TOKEN"
      ]
    },
    "leverage": {
      "description": "Leverage multiplier to apply before placing the order. TVH updates the exchange-side leverage for the pair only if the requested value differs from the current setting.",
      "type": "number",
      "examples": [
        "10"
      ]
    },
    "targets": {
      "description": "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.",
      "type": "array",
      "items": {
        "$ref": "#/$defs/LimitOrder"
      },
      "examples": [
        "[{\"takeProfitPercent\":1,\"amount\":50},{\"takeProfitPercent\":2,\"amount\":50}]"
      ]
    },
    "targetType": {
      "description": "Selects the target value field. `percent` = use `targets[].takeProfitPercent`; `absolute` = use explicit `targets[].price` levels (supports `takeProfitExpression`).",
      "type": "string",
      "default": "percent",
      "enum": [
        "percent",
        "absolute"
      ],
      "examples": [
        "percent"
      ]
    },
    "closeCurrentPosition": {
      "description": "Close the open position on the pair, regardless of direction. Combine with `useLimitClose` for a non-market exit.",
      "type": "boolean",
      "examples": [
        "true"
      ]
    },
    "marginType": {
      "description": "Per-pair margin mode for the trade. Switches the exchange setting before placing the order if required.",
      "type": "string",
      "enum": [
        "isolated",
        "cross"
      ],
      "examples": [
        "isolated"
      ]
    },
    "unitsType": {
      "description": "How to interpret `units`. `absolute` = base-asset quantity, `percent` = percent of available margin/balance, `percentBalance` = percent of total balance, `percentWallet` = percent of wallet equity, `percentPosition` = percent of current open position (close/scale-out only), `risk` = derive size from `stopLoss` distance.",
      "type": "string",
      "default": "absolute",
      "enum": [
        "absolute",
        "percent",
        "percentBalance",
        "percentWallet",
        "percentPosition",
        "risk"
      ],
      "examples": [
        "percent"
      ]
    },
    "unitsPercent": {
      "description": "Convenience field equivalent to `units` when `unitsType=\"percent\"`. Provided as a separate property so Pine alert messages can hard-code a percentage without flipping the type flag.",
      "type": "number",
      "examples": [
        "5"
      ]
    },
    "useTrailingStopLoss": {
      "description": "Convert the stop-loss into a trailing stop. TVH manages the trail off-exchange where the exchange has no native primitive.",
      "type": "boolean",
      "examples": [
        "true"
      ]
    },
    "stopLossToBreakEven": {
      "description": "Trailing-stop companion: when trailing SL is on, the stop locks at break-even (entry) once the trailing threshold is reached, instead of trailing further. Requires useTrailingStopLoss. Binance Futures and Bybit only.",
      "type": "boolean",
      "examples": [
        "true"
      ]
    },
    "trailingStopLossPercent": {
      "description": "Trail distance in percent. The stop follows price by this offset once activated.",
      "type": "number",
      "examples": [
        "0.7"
      ]
    },
    "trailingStopLossActivationPercent": {
      "description": "Profit threshold (percent from entry) before the trailing stop arms. Below this threshold the SL stays at its initial level.",
      "type": "number",
      "examples": [
        "0.5"
      ]
    },
    "preventPyramiding": {
      "description": "Hard guard: if a position is already open on the pair, reject the new entry. Prevents stacking on noisy alerts.",
      "type": "boolean",
      "examples": [
        "true"
      ]
    },
    "orderType": {
      "description": "TradingView strategy-alert direction field. Use `orderType` when mapping `{{strategy.order.action}}`; for normal webhook/manual TradeCommands use `isBuy: true`, `isSell: true`, or `isClose: true`.",
      "type": "string",
      "enum": [
        "buy",
        "sell",
        "close"
      ],
      "examples": [
        "buy"
      ]
    },
    "reduceOnly": {
      "description": "Mark the order as reduce-only — it can shrink but never increase a position. Used by SL/TP follow-ups and trailing-stop logic.",
      "type": "boolean",
      "examples": [
        "true"
      ]
    },
    "postOnly": {
      "description": "Reject the order if it would take liquidity (= ensures maker fee tier). Default is **true** in TVH; flip to false for limits that must fill aggressively.",
      "type": "boolean",
      "default": true,
      "examples": [
        "false"
      ]
    },
    "strategyComment": {
      "description": "Mirrors TradingView's `strategy.entry/exit(..., comment=...)`. TVH inspects the string for close tokens (`close`, `exit`, `flat`) to auto-trigger an exit, or for custom tokens listed in `strategyCloseValue`.",
      "type": "string",
      "examples": [
        "Long Exit TP1"
      ]
    },
    "useDca": {
      "description": "Enable the order-mesh entry style: initial market order plus N evenly-sized limit orders below (long) or above (short) entry.",
      "type": "boolean",
      "examples": [
        "true"
      ]
    },
    "dcaPercent": {
      "description": "Total spread of the DCA mesh from the initial entry, in percent. TVH spreads `dcaOrderCount` limits evenly across this range.",
      "type": "number",
      "examples": [
        "1.5"
      ]
    },
    "dcaOrderCount": {
      "description": "Number of follow-up limit orders in the DCA mesh (not counting the initial market order).",
      "type": "integer",
      "examples": [
        "5"
      ]
    },
    "limitPriceType": {
      "description": "How TVH derives the limit price. `fixedPrice` = use `price` verbatim. `bestPrice` = pin to the current best bid (buys) or best ask (sells) at submission time.",
      "type": "string",
      "default": "fixedPrice",
      "enum": [
        "fixedPrice",
        "bestPrice"
      ],
      "examples": [
        "bestPrice"
      ]
    },
    "broadcastSignal": {
      "description": "Name of the signal source to broadcast this trade to. Copy-trade followers of this source mirror the trade. Empty = no broadcast.",
      "type": "string",
      "examples": [
        "alpha-scalper"
      ]
    },
    "apiKey": {
      "description": "Name of the exchange API key registered in your TVH dashboard. Routes the order to the correct credentials.",
      "type": "string",
      "examples": [
        "binance-main"
      ]
    },
    "cancelAllDcaOrders": {
      "description": "Not an immediate cancel. A companion flag stored with the trade's trailing-stop record; when the trailing stop later closes the position, TVH also cancels the pair's remaining pending orders. Only meaningful alongside a trailing stop. For an immediate DCA-mesh cancel use `cancelAllDca`.",
      "type": "boolean",
      "examples": [
        "true"
      ]
    },
    "cancelAllOrders": {
      "description": "Cancel **every** open order on the pair (entries, DCA legs, TP, SL). Useful as a reset before a fresh setup.",
      "type": "boolean",
      "examples": [
        "true"
      ]
    },
    "hedgeMode": {
      "description": "Binance Futures dual-position mode. When true the account allows simultaneous long + short positions and routing uses `positionSide` instead of net direction.",
      "type": "boolean",
      "examples": [
        "true"
      ]
    },
    "closeLong": {
      "description": "Hedge-mode helper: close only the long side. Required when the account is in dual-position mode and you want to flat one direction.",
      "type": "boolean",
      "examples": [
        "true"
      ]
    },
    "closeShort": {
      "description": "Hedge-mode helper: close only the short side.",
      "type": "boolean",
      "examples": [
        "true"
      ]
    },
    "usePartialClose": {
      "description": "Close only a portion of the current position instead of the whole thing. Size of the cut comes from `partialCloseValue` + `partialCloseType`.",
      "type": "boolean",
      "examples": [
        "true"
      ]
    },
    "partialCloseValue": {
      "description": "Amount to close. Interpretation set by `partialCloseType` (percent of position vs absolute units).",
      "type": "number",
      "examples": [
        "50"
      ]
    },
    "useFixedSize": {
      "description": "Override `units`/`unitsType` with a fixed quote-currency amount from `fixedQuoteSize`. Useful for dollar-cost-style entries that ignore current balance.",
      "type": "boolean",
      "examples": [
        "true"
      ]
    },
    "fixedQuoteSize": {
      "description": "Quote-currency amount when `useFixedSize` is true (e.g. 100 = $100 of BTC at the current mark price).",
      "type": "number",
      "examples": [
        "100"
      ]
    },
    "conditionalPyramiding": {
      "description": "Soft guard: cap the position size on the pair. Stacking is allowed until the open position reaches `pyramidingValue`, then the new order is trimmed to the remaining capacity or rejected. Only inspects position size — no DCA-leg or order-count check.",
      "type": "boolean",
      "examples": [
        "true"
      ]
    },
    "pyramidingValue": {
      "description": "Maximum position size allowed on the pair for `conditionalPyramiding`. A base-asset quantity when `conditionalAsset=\"base\"`, otherwise a quote-currency value. Once reached, further entries are trimmed or rejected.",
      "type": "number",
      "examples": [
        "0.05"
      ]
    },
    "conditionalAsset": {
      "description": "Selects how `pyramidingValue` is read: `base` compares against the position's base-asset quantity, `quote` (or empty) against its quote-currency value. Always inspects the same pair's position, not a different asset.",
      "type": "string",
      "enum": [
        "base",
        "quote"
      ],
      "examples": [
        "base"
      ]
    },
    "targetAmountInPercent": {
      "description": "When true, each target's `amount` is a percent share of the position. When false, `amount` is an absolute quantity in base units.",
      "type": "boolean",
      "examples": [
        "true"
      ]
    },
    "closeInProfit": {
      "description": "Close the existing position only if it is currently in profit by at least `closeInProfitValue` (percent). Useful for break-even-protective exits.",
      "type": "boolean",
      "examples": [
        "true"
      ]
    },
    "margin": {
      "description": "Flag used by spot-margin adapters to mark the trade as a margin order rather than plain spot. Currently consumed by KuCoin margin and similar adapters.",
      "type": "boolean",
      "examples": [
        "true"
      ]
    },
    "useLimitClose": {
      "description": "When set together with a close command, exit the position using a limit order (price from `price` or chase logic) instead of a market order.",
      "type": "boolean",
      "examples": [
        "true"
      ]
    },
    "limitClosePercent": {
      "description": "Offset (percent from current price) for the limit-close order. Positive value pushes the close price into profit territory.",
      "type": "number",
      "examples": [
        "0.05"
      ]
    },
    "partialCloseType": {
      "description": "Interpret `partialCloseValue` as a percent of the position or as an absolute quantity.",
      "type": "string",
      "default": "percent",
      "enum": [
        "percent",
        "absolute"
      ],
      "examples": [
        "percent"
      ]
    },
    "partialCloseAsset": {
      "description": "When `partialCloseType=\"absolute\"`, choose whether the value is denominated in the `base` or `quote` asset.",
      "type": "string",
      "default": "quote",
      "enum": [
        "base",
        "quote"
      ],
      "examples": [
        "quote"
      ]
    },
    "stopLossSizeType": {
      "description": "Quantity model for the SL order. `currentPosition` = size matches whatever fills (preferred), `absolute` = explicit quantity supplied via `units`.",
      "type": "string",
      "default": "currentPosition",
      "enum": [
        "currentPosition",
        "absolute"
      ],
      "examples": [
        "currentPosition"
      ]
    },
    "cancelSlOrders": {
      "description": "Cancel all existing stop-loss orders for the pair before placing the new entry/SL. Prevents duplicate SLs when re-entering.",
      "type": "boolean",
      "examples": [
        "true"
      ]
    },
    "workingType": {
      "description": "Binance Futures only. Which price triggers SL/TP stop orders. `mark` is the index/mark price (safer); `contract` is the last trade price.",
      "type": "string",
      "default": "mark",
      "enum": [
        "mark",
        "contract"
      ],
      "examples": [
        "mark"
      ]
    },
    "closeInProfitValue": {
      "description": "Minimum profit (percent) required before `closeInProfit` triggers the exit. Below the threshold the close is skipped.",
      "type": "number",
      "examples": [
        "0.2"
      ]
    },
    "delay": {
      "description": "Seconds to wait before TVH forwards the signal to the exchange. Useful for staggered exits or letting the spread settle after news.",
      "type": "integer",
      "examples": [
        "5"
      ]
    },
    "strategyCloseValue": {
      "description": "Additional close token (or pipe-separated list) matched as a substring inside `strategyComment`, on top of the built-in `close`/`exit`/`flat` scan. It adds triggers, it does not replace the built-in ones. Example: `\"TP|SL|FLATTEN\"`.",
      "type": "string",
      "examples": [
        "close|exit|flat"
      ]
    },
    "retries": {
      "description": "How many times TVH re-sends an order when Binance returns an overload / server-busy response. Binance-specific overload guard, not a general cross-exchange retry; does not retry hard rejections or IP bans. Capped at 10.",
      "type": "integer",
      "default": 1,
      "examples": [
        "3"
      ]
    },
    "cancelAllDca": {
      "description": "Standalone trade command that cancels the pair's DCA mesh limit orders (TP/SL stay attached) and does nothing else. This is the field that actually clears a pending mesh immediately.",
      "type": "boolean",
      "examples": [
        "true"
      ]
    },
    "cancelAll": {
      "description": "Standalone command to cancel every pending order on the pair. Counterpart to `cancelAllOrders` when no entry is included.",
      "type": "boolean",
      "examples": [
        "true"
      ]
    },
    "cancelLimitOrderOffset": {
      "description": "Delay in seconds after which TVH auto-cancels the still-pending entry/DCA limit orders on the pair. Only runs with `isLimit=true` and a value greater than 0. TP/SL orders are untouched. Capped at 604800 s (7 days).",
      "type": "integer",
      "examples": [
        "300"
      ]
    },
    "useScaledOrders": {
      "description": "Enable scaled-orders ladder mode. Differs from DCA: you supply an explicit upper/lower price band rather than a percent spread, and there is no market component.",
      "type": "boolean",
      "examples": [
        "true"
      ]
    },
    "scaledUpperPrice": {
      "description": "Upper price bound of the scaled ladder. For shorts this is the highest entry; for longs the worst (highest) limit if the ladder lays out top-down.",
      "type": "number",
      "examples": [
        "66500"
      ]
    },
    "scaledLowerPrice": {
      "description": "Lower price bound of the scaled ladder.",
      "type": "number",
      "examples": [
        "64500"
      ]
    },
    "scaledOrderNumbers": {
      "description": "Number of limit orders to place across the upper/lower band.",
      "type": "integer",
      "examples": [
        "8"
      ]
    },
    "scaledOrderStyle": {
      "description": "Size distribution style across the ladder. `even` = each leg the same size. `bigSmall` = larger legs at the better-priced side, smaller legs at the worse. `smallBig` = the inverse.",
      "type": "string",
      "enum": [
        "even",
        "bigSmall",
        "smallBig"
      ],
      "examples": [
        "even"
      ]
    },
    "scaledUseCurrentPrice": {
      "description": "When true, treat the current mark/last price as one bound and use only `scaledUpperPrice` **or** `scaledLowerPrice` as the other bound (whichever is set).",
      "type": "boolean",
      "examples": [
        "true"
      ]
    },
    "updateCurrentLimitOrder": {
      "description": "Reposition the open limit order nearest to market (not the most recent one) to the current best bid/ask. Cancels and re-places it, keeping size and leverage. Ignores `price` — it can only snap to the touch.",
      "type": "boolean",
      "examples": [
        "true"
      ]
    },
    "instrumentType": {
      "description": "OKX-only product family. Controls which OKX endpoint is used for the order.",
      "type": "string",
      "enum": [
        "spot",
        "margin",
        "futures",
        "swap",
        "options"
      ],
      "examples": [
        "swap"
      ]
    },
    "positionSide": {
      "description": "OKX-only side tag. `net` for one-way mode, `long` or `short` when the account runs hedge-mode separately.",
      "type": "string",
      "default": "net",
      "enum": [
        "net",
        "long",
        "short"
      ],
      "examples": [
        "net"
      ]
    },
    "targetAssignedToPosition": {
      "description": "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.",
      "type": "boolean",
      "examples": [
        "true"
      ]
    },
    "useEntireAccountBalance": {
      "description": "Bybit UTA-only shortcut to size the entry using the full available balance after reservations. Ignores `units` and `fixedQuoteSize`.",
      "type": "boolean",
      "examples": [
        "true"
      ]
    },
    "chaseLimitOrder": {
      "description": "Re-place the limit order at the moving best price until it fills, expires, or the chase budget runs out. Default budget: 180 attempts × 20 s = 1 hour.",
      "type": "boolean",
      "examples": [
        "true"
      ]
    },
    "updateLimitOrderBuyMarketAfterNotFilled": {
      "description": "Fallback to market order once the chase budget is exhausted. Otherwise the order is simply cancelled.",
      "type": "boolean",
      "examples": [
        "true"
      ]
    },
    "updateLimitOrderTimeInterval": {
      "description": "Seconds to wait between chase attempts. Default 20 s; the chase runs for up to 180 attempts (~1 hour budget).",
      "type": "integer",
      "default": 20,
      "examples": [
        "20"
      ]
    },
    "updateStopLossToBreakEven": {
      "description": "Standalone command that moves an open position's existing SL to its average entry price (break-even). No new entry placed. Only fires when the position is in profit and already has an SL; not supported on every exchange.",
      "type": "boolean",
      "examples": [
        "true"
      ]
    },
    "alertTimestamp": {
      "description": "Idempotency key. TVH builds a deduplication key from `(token, apiKey-or-exchange, alertTimestamp)` the first time it processes the payload; a later payload with the same triple is recognised as a duplicate and silently dropped. Recommended Pine value: `\"{{ticker}}-{{timenow}}\"`.",
      "type": "string",
      "examples": [
        "BTCUSDT-1715900400000"
      ]
    },
    "setTpToPosition": {
      "description": "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.",
      "type": "boolean",
      "examples": [
        "true"
      ]
    }
  },
  "required": [
    "exchange",
    "pair",
    "token",
    "apiKey"
  ],
  "$defs": {
    "LimitOrder": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "idx": {
          "type": "integer",
          "description": "Position of the target inside the ladder. Used by the UI / activity log to label fills (TP1, TP2, ...). Zero-based."
        },
        "price": {
          "type": "number",
          "description": "Absolute target price. Use when the parent TradeCommand has `targetType=absolute`. For `targetType=percent`, use `takeProfitPercent` instead."
        },
        "amount": {
          "type": "number",
          "description": "Size assigned to this target. Percent of position when `targetAmountInPercent=true`, otherwise absolute base-asset quantity."
        },
        "takeProfitPercent": {
          "type": "number",
          "description": "Target distance in percent from entry. Use when the parent TradeCommand has `targetType=percent`. For `targetType=absolute`, use `price` instead."
        },
        "trailingPercent": {
          "type": "number",
          "description": "Per-target trailing offset. Once price reaches this target, the trailing logic for that slice arms with this distance."
        }
      }
    }
  }
}