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": "percentBalance",
"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": "percentBalance",
"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": "percentBalance",
"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.