# Alert messages

Source: https://docs.pipsocket.com/tradingview-messages

Every field of a hook's message, what each action does, and how sizes, levels and order are worked out.

A hook's message is a JSON object. The dashboard's message builder writes it for you; this page is the reference for what it can say.

```json
{
  "action": "open",
  "src": "q7m2",
  "event_id": "q7m2-ema-{{exchange}}:{{ticker}}-{{interval}}-open-buy-{{time}}",
  "sent_at": "{{timenow}}",
  "symbol": "{{exchange}}:{{ticker}}",
  "side": "buy",
  "tag": "ema",
  "risk": { "percent": 1 },
  "sl": { "pips": 25 },
  "tp": { "pips": 50 }
}
```

- **No version field.** The format only ever gains optional fields.
- **Unknown fields are refused** (`unknown_field`), so a typo never fails silently.
- **Numbers may be strings**, as TradingView's placeholders arrive as text (`"lots": "{{strategy.order.contracts}}"` is fine), but must be finite numbers. `"ten"` or `"inf"` is refused `invalid_field`.

## Fields

| Field | Meaning |
| --- | --- |
| `action` | `open`, `close`, `close_all`, `cancel`, `reverse`, `modify`, `strategy` or `kill_switch`. See [actions](#actions). |
| `event_id` | Up to 128 characters: the alert's id, for spotting duplicates within the hook for 24 hours. See [ids, duplicates and order](#ids-duplicates-and-order). |
| `src` | A short id the builder makes for each alert template, part of its `event_id`s, so two templates never share ids. |
| `sent_at` | When the alert was sent: `{{timenow}}` (UTC, to the second, such as `2026-11-08T14:30:00Z`). Used for age limits and order. Left out: when Pipsocket received it. |
| `source_seq` | A whole number that grows with every alert the source sends for the lane. When present, it decides order exactly. In an `alert()` message, Pine's `timenow` in milliseconds works only when each of the lane's alerts comes from its own run, well apart (such as once per bar close): alerts from one run, or from runs in the same millisecond, share it. Otherwise leave it out. |
| `max_age` | Seconds: overrides the hook's [age limit](#age-limits) for this alert's entries. |
| `symbol` | `{{exchange}}:{{ticker}}`, or a bare ticker. See [symbols](/tradingview#symbols). |
| `side` | `buy` or `sell`. `close`, `cancel` and `modify` also take `all`. |
| `lots` or `risk` | The size. See [sizing](#sizing). |
| `entry` | A pending entry instead of a market order. See [pending entries](#pending-entries). |
| `sl`, `tp` | Stop loss and take profit. See [levels](#levels); `tp` may be a [ladder](#take-profit-ladders). |
| `trailing`, `break_even` | Managed stops. See [trailing stops and break-even](#trailing-stops-and-break-even). In `modify`, `"none"` removes them. |
| `portion` | How much to close: `{"percent": n}` or `{"lots": n}`. See [partial closes](#partial-closes). |
| `tag` | Up to 16 characters of `A-Z`, `a-z`, `0-9`, `_` and `-`: the [lane](#tags-and-lanes) the alert acts in. |
| `position` | Strategy mode's position. See [strategy mode](#strategy-mode). |
| `accounts` | A list of the hook's targets (account ids or target short names): the alert acts only on these. |
| `slippage` | A whole number of points: the deviation allowed for market orders. Left out: the target's setting. |
| `comment` | Your note, kept in the alert log only. |
| `test` | `true` makes the alert a [dry run](#dry-runs). |

### Which fields each action takes

R is required, O optional, and a blank is refused. Every action also takes `event_id`, `sent_at`, `source_seq`, `src`, `accounts`, `comment` and `test`.

| `action` | `symbol` | `side` | `lots` / `risk` | `entry` | `sl` | `tp` | `trailing` / `break_even` | `portion` | `tag` | `position` | `max_age` | `slippage` |
| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |
| `open` | R | R | O (1) | O | O | O | O | | O | | O | O |
| `close` | R | O | | | | | | O | O | | | O |
| `close_all` | | | | | | | | | O | | | O |
| `cancel` | O | O | | | | | | | O | | | |
| `reverse` | R | R | O (1) | | O | O | O | | O | | O | O |
| `modify` | R | O | | O (2) | O | O | O | | O | | | |
| `strategy` | R | | O (4) | | O | O (3) | O | | O | R | O | O |
| `kill_switch` | | | | | | | | | | | | |

1. Left out, the target's fixed lots if it sizes fixed, else its default lots; refused if it has neither.
2. Moves a pending order's price.
3. A single take profit, not a ladder.
4. `lots` only: the target's [strategy sizing](#strategy-mode) decides, and the message's lots only fill a gap.

`modify` needs at least one of `entry`, `sl`, `tp`, `trailing` and `break_even`.

### Numbers

| Rule | Fields |
| --- | --- |
| More than 0 | `lots`, `risk.amount`, `portion.lots`, distances in levels (`pips`, `points`, `percent`), prices, `trailing.distance_pips`, `trailing.step_pips`, `break_even.trigger_pips`, `max_age` |
| 0 or more | `entry.offset_pips` and `entry.offset_points` (0 is the current price), `entry.limit_offset_pips`, `break_even.offset_pips`, `trailing.start_pips`, `slippage`, `position.size` and `position.from_size` |
| More than 0, at most 100 | `risk.percent`, `portion.percent`, a ladder rung's `part` |
| Exactly one of | `lots` or `risk`; in `risk`, `percent` or `amount`; in a level, `pips`, `points`, `price` or `percent`; in `portion`, `percent` or `lots`; in `entry`, `price`, `offset_pips` or `offset_points` |
| Whole number, larger than the last | `source_seq`, per lane |
| Strategy positions agree | `to` is `flat` exactly when `size` is 0, and `from` is `flat` exactly when `from_size` is 0; otherwise the sizes are more than 0 |

## Actions

Every action acts only on its own [lane's](#tags-and-lanes) positions and pending orders, except `kill_switch`.

### open

A market order, or a pending order with `entry`. Before it opens, it checks the lane:

- **Same side already held** (positions, pending orders, or entries still waiting for an answer): refused `pyramiding`, unless the target's pyramiding allows more entries.
- **Other side held**: refused `opposite_open` by default. With the target's opposite side set to close first, the open runs as a [reverse](#reverse); set to hedge, it opens beside it (hedging accounts only).

Then it sizes the order, rounds and checks the stops, asks MT5 whether the account has the margin (`OrderCheck`), and sends it with the stops in the request. An open never shrinks its size or widens its stops to fit: it is refused instead.

```json
{ "action": "open", "symbol": "OANDA:EURUSD", "side": "buy", "lots": 0.10, "sl": { "pips": 20 }, "tp": { "pips": 40 } }
```

### close

Closes the lane's positions, all of them or one side's (`side`), in full or in part (`portion`). Positions are closed oldest first, and the account is read again before each one. It never opens or adds, and never touches pending orders.

```json
{ "action": "close", "symbol": "OANDA:EURUSD", "side": "buy", "portion": { "percent": 50 } }
```

### close_all

Cancels the pending orders of all the hook's lanes on the account (or of one `tag`), then closes every position they hold, checking for orders that filled while being cancelled. No `symbol`: it covers every symbol. It touches nothing the hook doesn't own.

```json
{ "action": "close_all", "tag": "ema" }
```

### cancel

Deletes the lane's pending orders, narrowed by `symbol` and `side` if given. An order that fills while it's being cancelled is reported, and its position stays open.

```json
{ "action": "cancel", "symbol": "OANDA:EURUSD", "side": "buy" }
```

### reverse

Moves the lane to the other side: it cancels the lane's pending entries, closes the positions on the other side, waits until every close is confirmed filled in full, reads the lane again, and opens `side` only if the lane then holds nothing on the other side and the age limit and kill switch still allow it.

If any close is refused, only partly filled or has no answer, or anything remains on the other side, the open is never sent, and a confirmation that arrives later doesn't bring it back. The alert ends `partial` when part of it was done, `unknown` while a step still has no answer, or `refused` when nothing was.

```json
{ "action": "reverse", "symbol": "OANDA:EURUSD", "side": "sell", "lots": 0.10, "sl": { "pips": 20 } }
```

### modify

Changes the levels of the lane's positions and pending orders (narrowed by `side`): each is read fresh, only the levels you give change, the rest are kept, and the new ones are checked as in [levels](#levels). `entry` moves a pending order's price. `trailing` and `break_even` replace the managed stops of those positions, and `"none"` removes them. It never changes a size.

```json
{ "action": "modify", "symbol": "OANDA:EURUSD", "side": "buy", "sl": { "price": 1.0832 }, "trailing": "none" }
```

### strategy

Brings the lane to the strategy's position. See [strategy mode](#strategy-mode).

### kill_switch

Engages the account's [kill switch](/api/kill-switch), only on targets whose allowed actions include it (off by default). See [the kill switch and alerts](/tradingview#the-kill-switch-and-alerts).

```json
{ "action": "kill_switch", "event_id": "panic-{{timenow}}" }
```

## Sizing

| Message | Size |
| --- | --- |
| `"lots": 0.10` | Broker lots. |
| `"risk": {"percent": 1}` | Lots such that a stop loss filled at the stop loses about 1% of the target's basis (balance or equity, a target setting). Needs `sl`. |
| `"risk": {"amount": 50}` | The same, as an amount in the account's currency. Needs `sl`. |

How a size is worked out, in order:

1. The stops are rounded and checked first ([levels](#levels)); risk sizing uses the rounded stop loss.
2. For risk: the loss per lot at the stop, from MT5's own profit calculation (`OrderCalcProfit`) in the account's currency. If MT5 can't work it out, or it isn't a loss, the alert is refused `risk_unavailable`. Lots = risk ÷ loss per lot.
3. The target's sizing: **as sent**, **multiplier** (lots × your number), or **fixed** (your lots, whatever the message says).
4. The target's maximum lots: above it is refused `above_max_lots`, or cut down to it if the target allows clipping.
5. Rounded down to the symbol's lot step. Below the minimum lot is refused `below_min_lot`.
6. MT5 checks the margin: refused `no_margin` if the account can't afford it.

The alert log shows each step and the estimated loss at the stop for the final size. It is an estimate: gaps, slippage, commission and swap aren't in it.

## Levels

A stop loss or take profit can be given four ways (a pending order's price has its own fields: see [pending entries](#pending-entries)):

| Form | Meaning |
| --- | --- |
| `{"pips": n}` | A distance in pips from the base price (the pip size is the symbol map's). |
| `{"points": n}` | A distance in the symbol's points. |
| `{"price": p}` | A price. |
| `{"percent": x}` | A distance as a percent of the base price (not of the account). |

- **The base price** for distances: for a market order, the price it would fill at when the alert is worked out (ask for buys, bid for sells); for a pending order, its order price (a stop limit's limit price); in `modify`, a position's open price or a pending order's price.
- **Distances are always on the protective side**: a buy's stop loss below the base and take profit above, a sell's the other way round. To put a stop beyond the entry, locking in profit, give a `price` or use [break-even](#trailing-stops-and-break-even).
- **Rounding**: to the symbol's tick size, never closer to the base than you asked (at most one tick further).
- **New orders** (open, pending entries, ladder rungs): a `price` must be on the protective side of the base too, or the alert is refused `invalid_level`.
- **Existing positions** (`modify`, managed stops): levels may cross the open price. They are checked against the market, as MT5 does: a buy's stop loss below bid minus the stops level and take profit above bid plus it, a sell's stop loss above ask plus the stops level and take profit below ask minus it, and nothing within the freeze level of the price that would trigger it.
- **Pending orders**: the order price is checked against the market by the stops and freeze levels, and its stop loss and take profit against the order price by the stops level.

A level that fails is refused `stop_too_close`, `frozen` or `invalid_level`, never moved to fit.

## Pending entries

`entry` makes an open a pending order:

```json
{
  "action": "open",
  "symbol": "OANDA:XAUUSD",
  "side": "buy",
  "lots": 0.10,
  "entry": { "type": "limit", "offset_pips": 15, "expires": "day" },
  "sl": { "pips": 30 },
  "tp": { "pips": 60 }
}
```

| Field | Meaning |
| --- | --- |
| `type` | `limit`, `stop` or `stop_limit`. |
| `price`, `offset_pips` or `offset_points` | The order price, or its distance from the current price (below). |
| `limit_price` or `limit_offset_pips` | `stop_limit` only: the limit price, or its distance from the trigger price. |
| `expires` | `gtc` (until cancelled), `day`, or a UTC time such as `2026-11-08T21:00:00Z`. |

Offsets are measured from the current price, in the direction each order needs:

| Order | Offset from | Direction |
| --- | --- | --- |
| Buy limit | Ask | Below |
| Sell limit | Bid | Above |
| Buy stop | Ask | Above |
| Sell stop | Bid | Below |
| Buy stop limit | Ask | Trigger above; limit at or below the trigger |
| Sell stop limit | Bid | Trigger below; limit at or above the trigger |

- **Expiry** must be one the symbol allows, or the alert is refused `expiry_not_allowed`. A UTC time is converted to the broker's server time.
- **Filling**: pending orders use MT5's return mode. Market orders use fill or kill or immediate or cancel, whichever the symbol allows (return only where its execution mode permits); if none fits, the alert is refused `filling_unavailable`. A market order that leaves part unfilled is reported as a partial fill, and Pipsocket asks the broker at once to cancel the rest. Should that cancel be refused or go unanswered, the rest stays a live order of the lane and may still fill: Pipsocket keeps checking the account and tries the cancel again (once the earlier attempt has settled, at most once a minute), a later `close` without `portion`, `close_all`, `reverse` or strategy-mode move of the lane on the symbol cancels it too (a `close` with a `portion` doesn't), and the alert stays watched until it is gone.
- **After placing**, a pending order's outcome is followed through: placed, changed, cancelled, expired, filled, partly filled (the rest stays a live order of the lane) or rejected. Its ownership, stops and managed stops last until the order ends and any position it opened closes, even after the alert has left the 30-day log.
- An alert that stops part way keeps the pending orders it already placed (reported, and yours to cancel). Only `cancel`, `reverse`, `close_all` and strategy mode cancel pending orders.

## Partial closes

`portion` on a `close` is across all the positions selected, not each one:

- `{"lots": V}` closes V lots in total; `{"percent": p}` closes p% of the selection's total volume. Volumes are rounded down to the lot step.
- Positions are closed oldest first, each by as much as is left to close. If a close would leave less than the minimum lot in a position, it closes less, so at least the minimum stays (or skips that position), and moves on to the next.
- The account is read again before each close, and the alert log shows what was asked for and what actually closed.

## Take-profit ladders

On hedging accounts, a market entry can take a ladder of 2 to 10 rungs as its `tp`, each a [level](#levels) with a `part`. Ladders are for market entries only: a ladder on a pending entry is refused `invalid_field`.

```json
{
  "action": "open",
  "symbol": "OANDA:EURUSD",
  "side": "buy",
  "lots": 0.30,
  "sl": { "pips": 20 },
  "tp": [{ "pips": 20, "part": 50 }, { "pips": 40, "part": 30 }, { "pips": 60, "part": 20 }]
}
```

- The parts are percents of the size and must add up to exactly 100.
- Each rung is its own position, with its own take profit and the message's stop loss. A stop loss given as a distance is worked out from the price read for each rung as it is sent, so rungs sent at different prices get stops at different prices. Its lots are the size × part ÷ 100, rounded down to the lot step; what rounding leaves over goes to the first rung. Every rung must be at least the minimum lot, or the open is refused.
- Rungs are sent in order. If one is refused, only partly filled or has no answer, the later rungs aren't sent. The alert ends `partial` showing what filled, `unknown` while a rung still has no answer, or `refused` when none filled. Missing volume is never sent again.

## Trailing stops and break-even

```json
{
  "action": "open",
  "symbol": "OANDA:EURUSD",
  "side": "buy",
  "lots": 0.10,
  "sl": { "pips": 25 },
  "trailing": { "distance_pips": 20, "step_pips": 2, "start_pips": 10 },
  "break_even": { "trigger_pips": 15, "offset_pips": 1 }
}
```

| Field | Meaning |
| --- | --- |
| `trailing.distance_pips` | How far behind the best price the stop loss is kept: for a buy, below the highest bid since trailing began; for a sell, above the lowest ask. |
| `trailing.start_pips` | Optional: how far in profit the position must be before trailing begins. Until then the trailing rule does nothing. |
| `trailing.step_pips` | Optional: the smallest move Pipsocket makes. The stop loss moves only when the new level is at least this much better than the current one. |
| `break_even.trigger_pips` | How far in profit the position must be before the stop loss moves, once, to break-even. |
| `break_even.offset_pips` | Optional: where past the entry it goes: a buy's entry plus the offset, a sell's entry minus it. |

- When both rules are set, the more protective level wins.
- **The broker holds the stop loss.** Pipsocket moves it, but the stop itself is on the position at the broker, so if Pipsocket or the terminal is down, the last stop stays in place.
- **It only ever tightens.** If the stop loss is already at or beyond where the rule would put it, whether you moved it with the API or by hand, nothing is sent. The take profit is kept as it is.
- A move that is too close to the price, or frozen, is tried again on a later price. At most one move per second per position, each one confirmed by the broker before the next.
- **A move with no answer from MT5 in time is unknown**: nothing more moves on that position until it settles. The account's page lists it to settle by hand. See [unknown outcomes](/tradingview-outcomes#unknown-outcomes).
- **Without an `sl`**, nothing protects the position until a rule first sets one; the builder warns you.
- Rules last for the position's life: a partial close keeps them on what's left, `modify` with `"none"` removes them, and closing the position ends them. Pausing or deleting the hook doesn't stop them, and they carry on across restarts. A change of the account's login or server ends them.

## Strategy mode

`"action": "strategy"` follows a TradingView strategy's position instead of acting on each order. The message says where the strategy was and where it is now, from the strategy's placeholders:

```json
{
  "action": "strategy",
  "src": "k3f9",
  "event_id": "k3f9-ema-{{exchange}}:{{ticker}}-{{interval}}-{{strategy.order.id}}-{{time}}-{{strategy.market_position}}-{{strategy.market_position_size}}",
  "sent_at": "{{timenow}}",
  "symbol": "{{exchange}}:{{ticker}}",
  "tag": "ema",
  "position": {
    "from": "{{strategy.prev_market_position}}",
    "from_size": "{{strategy.prev_market_position_size}}",
    "to": "{{strategy.market_position}}",
    "size": "{{strategy.market_position_size}}"
  },
  "sl": { "pips": 25 },
  "tp": { "pips": 50 }
}
```

`from` and `to` are `long`, `short` or `flat`. Pipsocket compares the move the strategy made with what the lane holds now (H) and what it should have held before the move (P):

| The strategy's move | What Pipsocket does |
| --- | --- |
| Increase: same direction and bigger, or flat to in | If H matches P (within one lot step), opens the difference. Otherwise the account has drifted, and nothing opens (`drift`), unless the target heals drift: then it opens up to the target. |
| Reduction: same direction, smaller | Closes down toward the target, never past it. If H is already at or below the target, nothing (`drift` if below). Never opens. |
| Exit: to flat | Closes all the lane's positions. |
| Flip: long to short or short to long | Closes the lane's positions, confirms the closes, reads the lane again, and opens the target size on the other side only if nothing is left on the old side and H matched P (or the target heals drift). Otherwise it stops without opening. Its state is worked out as for any alert: `unknown` while a close still has no answer, then `refused` when nothing was done or `partial` when part was, with the failed step's reason; when its closes went through and only H not matching P stopped it, the reason is `drift`. |

- **Sizing**: the target's strategy sizing decides. **Fixed**: every position is the target's fixed lots (or the message's `lots`), so only direction changes matter. **Scaled**: lots = the strategy's size × the target's lots per contract, rounded down to the lot step. Pyramiding and opposite side settings don't apply.
- **A strategy lane is strategy mode only**: `open`, `reverse` and pending entries in the same lane are refused `lane_in_strategy_mode`.
- Before acting, the lane's pending orders are cancelled.
- An exit never turns into a position on the other side.

## Tags and lanes

A lane is one account, one hook, one `tag` and one symbol. Ownership, order and strategy mode are all per lane:

- A hook only acts on positions and pending orders its own lane opened. Your manual and API trades, and other lanes', are never touched (except by the kill switch).
- Several strategies can share a hook and an account with different tags. Without a `tag`, a message acts in the hook's untagged lane.
- `close_all` with a `tag` covers that tag's lanes; without one, all the hook's lanes on the account.
- `accounts` narrows an alert to some of the hook's targets.

## Ids, duplicates and order

TradingView can deliver an alert twice, and alerts fired close together can arrive out of order. Three fields deal with that.

### event_id and duplicates

- An alert with the same `event_id` as one the hook received in the last 24 hours, and the same message, is refused `duplicate`; with a different message, `conflict`.
- Without an `event_id`, an identical message within 60 seconds counts as a duplicate.
- The builder's `event_id`s are made of the template's `src`, the tag, the symbol, the interval and the bar's time, so different lanes and templates never collide:
  - indicators: `{src}-{tag}-{{exchange}}:{{ticker}}-{{interval}}-{action}-{side}-{{time}}`
  - strategies: `{src}-{tag}-{{exchange}}:{{ticker}}-{{interval}}-{{strategy.order.id}}-{{time}}-{{strategy.market_position}}-{{strategy.market_position_size}}`
- These assume at most one alert per bar for each lane and action. A strategy that fills several times in one bar adds a value from Pine through `{{strategy.order.alert_message}}`, or uses an `alert()` message: see [order fills](/tradingview#order-fills-strategies).

### The ordering rule for late exits

An exit (`close`, `close_all`, `cancel`, strategy reductions and exits, and the close in a `reverse` or flip) only acts on positions and orders opened by alerts that came **before** it:

1. With `source_seq` on both alerts: opened earlier if its `source_seq` is lower. Exact.
2. Otherwise, with `sent_at` on both, in different seconds: opened earlier if its `sent_at` is earlier.
3. Otherwise: opened earlier if Pipsocket received it first.

So a close that arrives late never closes an entry that arrived after it, and with `source_seq`, never one that was sent after it. Without `source_seq`, two alerts of one lane sent in the same second (or without `sent_at`) are put in the order they were received, which TradingView may not keep; the alert log flags an exit decided that way `ambiguous_order`, whether or not they actually arrived out of order. If your strategy can fire twice in a second, send `source_seq`.

### Age limits

An alert that would open or add is refused `stale` when it is older than the hook's age limit (30 seconds by default, or the message's `max_age`), measured from `sent_at`, or from when Pipsocket received it. An entry that was right a minute ago may not be now.

Exits are never refused for age: a late close still closes, under the ordering rule above.

## Dry runs

`"test": true`, or **Send test** in the dashboard, runs everything up to sending: parsing, the symbol map, sizing, levels, and MT5's margin check for the first order that would open, against the account as it is now. What would follow (an open after a close, later ladder rungs) is reported as "would then ..." without those checks.

A dry run never trades, never engages the kill switch, never starts trailing stops, and doesn't count as a duplicate of a live alert.

