Skip to main content

Multiple Alerts at Once + Timing Controls

TL;DR
  • Send multiple TradeCommands in one alert by wrapping them in square brackets: [ {...}, {...} ].
  • delay: <seconds> postpones execution (up to 604,800s = 7 days).
  • delay: -1 is a special value that overrides the default 5-second email-backup delay (immediate execution from an email-triggered signal).
  • alertTimestamp is the idempotency key — the same value is treated as a duplicate and the second execution is skipped permanently.

Square-bracket batch syntax

A standard alert message is a single JSON object:

{ "pair": "BTCUSDT", "isBuy": true, ... }

For multiple commands, wrap them in square brackets and comma-separate:

[
{ "pair": "ETHUSD", "units": 50, "unitsType": "absolute", "exchange": "Bybit", "apiKey": "Key1", "isBuy": true, "isMarket": true, "leverage": 10, "token": "{{strategy.order.alert_message}}", "alertTimestamp": "{{ticker}}-ETH-{{timenow}}" },
{ "pair": "BTCUSD", "units": 500, "unitsType": "absolute", "exchange": "Bybit", "apiKey": "Key2", "isBuy": true, "isMarket": true, "leverage": 10, "token": "{{strategy.order.alert_message}}", "alertTimestamp": "{{ticker}}-BTC-{{timenow}}" }
]

Both commands fire in order against the TVH backend. Each gets its own queue slot.

Use cases for batch

ScenarioWhat you do
Pair tradeLong X + Short Y in one alert
Multi-exchangeSame trade on Binance + Bybit simultaneously
Multi-accountTrigger same strategy across sub-accounts using different apiKey values
Open + bracket-ordersEntry + standalone SL update + TP modification in one alert
Hedge mode pairsLong leg + short leg of a hedge on the same pair

Pine snippet for batch

Pine doesn't have a JSON array literal, so you concatenate strings:

//@version=5
strategy("Batch Pair Trade", overlay=true)

cmd1 = '{"pair":"ETHUSD","isBuy":true,"isMarket":true,"unitsType":"absolute","units":50,"leverage":10,"exchange":"Bybit","apiKey":"Key1","token":"{{strategy.order.alert_message}}","alertTimestamp":"{{ticker}}-ETH-{{timenow}}"}'
cmd2 = '{"pair":"BTCUSD","isBuy":true,"isMarket":true,"unitsType":"absolute","units":500,"leverage":10,"exchange":"Bybit","apiKey":"Key2","token":"{{strategy.order.alert_message}}","alertTimestamp":"{{ticker}}-BTC-{{timenow}}"}'

if (pairTradeSignal)
alert("[" + cmd1 + "," + cmd2 + "]", alert.freq_once_per_bar_close)

Always give each command a unique alertTimestamp — otherwise the idempotency check might skip one of them.

Delayed execution

delay postpones execution by <seconds>. Maximum: 604,800 seconds (7 days).

{
"pair": "BTCUSDT",
"isBuy": true,
"isMarket": true,
"delay": 30,
"unitsType": "percent",
"unitsPercent": 95,
"leverage": 5,
"exchange": "BinanceFutures",
"apiKey": "MyKey",
"token": "{{strategy.order.alert_message}}",
"alertTimestamp": "{{ticker}}-{{timenow}}"
}

This trade fires 30 seconds after the webhook arrives. Common use:

  • Wait for a confirmation candle before entering
  • Stagger entries across a basket of pairs
  • Avoid front-running another bot in the same account

Practical: auto-cancel an unfilled limit

Pair a limit entry with a delayed cancel to give the order a time limit. Send both in one batch: the first command places the limit, the second waits 30 seconds and then cancels any orders still pending on the pair.

[
{
"pair": "BTCUSDT",
"exchange": "binance-futures",
"apiKey": "MyKey",
"isBuy": true,
"isLimit": true,
"price": "{{close}}",
"postOnly": false,
"unitsType": "percent",
"unitsPercent": 5,
"token": "YOUR_TOKEN",
"alertTimestamp": "{{ticker}}-entry-{{timenow}}"
},
{
"pair": "BTCUSDT",
"exchange": "binance-futures",
"apiKey": "MyKey",
"cancelAllOrders": true,
"delay": 30,
"token": "YOUR_TOKEN",
"alertTimestamp": "{{ticker}}-cancel-{{timenow}}"
}
]

If the limit fills within 30 seconds, the cancel finds nothing to remove and is a no-op. If it does not fill, the cancel pulls the resting order so a missed entry never lingers. That is a simple time-in-force you control straight from the alert.

Email-backup override (delay: -1)

TVH supports a secondary email-based delivery for webhooks that get lost. By default the email-triggered signal waits 5 seconds before executing — giving the webhook a chance to land first.

If you want the email-triggered signal to execute immediately (no 5-second wait), set delay: -1:

{
"pair": "BTCUSDT",
"isBuy": true,
"isMarket": true,
"delay": -1,
"unitsType": "percent",
"unitsPercent": 95,
"exchange": "BinanceFutures",
"apiKey": "MyKey",
"token": "{{strategy.order.alert_message}}",
"alertTimestamp": "{{ticker}}-{{timenow}}"
}

For the full email-backup workflow, see E-Mail Signals.

Idempotency via alertTimestamp

alertTimestamp is your deduplication key. TVH stores the timestamp value the first time it sees it and silently skips any subsequent command with the same value.

Recommended Pine pattern:

"alertTimestamp": "{{ticker}}-{{timenow}}"

This combines the ticker symbol (so different pairs don't collide) with {{timenow}} (the bar's open time in ms) — guaranteeing uniqueness per pair per bar.

Permanent dedup lock — not a 5-minute window

The alertTimestamp lock is permanent for the lifetime of the key. If you send the same timestamp twice, the second one is ignored forever, not just for 5 minutes. This is critical for the email-backup path: webhook + email both fire with the same alertTimestamp, so only one of them ever executes.

The Trade Command Builder auto-adds "alertTimestamp": "{{ticker}}-{{timenow}}" to every new command in current versions. Legacy setups (commands generated by older builder revisions, or hand-written JSON) may not have this field — add it manually, especially when using email-backup signals.

Combining batch + delay + idempotency

A full example: pair-trade signal that fires both legs 30 seconds later, each with its own unique idempotency key:

[
{
"pair": "ETHUSD",
"isBuy": true,
"isMarket": true,
"unitsType": "absolute",
"units": 50,
"leverage": 10,
"delay": 30,
"exchange": "Bybit",
"apiKey": "Key1",
"token": "{{strategy.order.alert_message}}",
"alertTimestamp": "{{ticker}}-ETH-long-{{timenow}}"
},
{
"pair": "BTCUSD",
"isSell": true,
"isMarket": true,
"unitsType": "absolute",
"units": 500,
"leverage": 10,
"delay": 30,
"exchange": "Bybit",
"apiKey": "Key2",
"token": "{{strategy.order.alert_message}}",
"alertTimestamp": "{{ticker}}-BTC-short-{{timenow}}"
}
]

Both legs queue at the same time, both execute ~30 seconds later, and each has its own dedup key.

Pine snippet — batched delayed pair trade

//@version=5
strategy("Delayed Pair Trade", overlay=true)

cmd1 = '{"pair":"ETHUSD","isBuy":true,"isMarket":true,"unitsType":"absolute","units":50,"leverage":10,"delay":30,"exchange":"Bybit","apiKey":"Key1","token":"{{strategy.order.alert_message}}","alertTimestamp":"{{ticker}}-ETH-long-{{timenow}}"}'
cmd2 = '{"pair":"BTCUSD","isSell":true,"isMarket":true,"unitsType":"absolute","units":500,"leverage":10,"delay":30,"exchange":"Bybit","apiKey":"Key2","token":"{{strategy.order.alert_message}}","alertTimestamp":"{{ticker}}-BTC-short-{{timenow}}"}'

if (pairSignal)
alert("[" + cmd1 + "," + cmd2 + "]", alert.freq_once_per_bar_close)

Common pitfalls

  • Shared alertTimestamp across batch entries → the second entry is deduped and never executes.
  • delay beyond 604,800s → rejected.
  • delay: -1 outside of email-backup context → has no useful effect on webhook signals; reserved for the email path.
  • Missing alertTimestamp entirely → no idempotency. A retry or double-fire opens two positions.
  • JSON-array typos → forgetting the outer [ ] or missing comma between objects causes the entire batch to fail-to-parse.

For the complete timing field spec, see Timing & Idempotency Fields.