# EABuilder Strategy Spec — Authoring Guide (v1.0)

This is the contract between **you (or your LLM)** and **EABuilder**. You describe a
trading strategy as a single JSON document; EABuilder validates it and generates a
compilable MT5 Expert Advisor (`.mq5`) from it, which then runs on EAHosting.

> **Golden rule:** anything not described in this guide or in `schema/strategy.schema.json`
> is **not allowed**. There is no raw-MQL5 escape hatch. If you need behaviour that
> isn't here, it has to become a new block — request it; don't try to smuggle code in.

The machine-readable contract is `schema/strategy.schema.json` (JSON Schema 2020-12).
This document explains the parts a schema can't: the expression grammar, the operand
namespace, and the rules of the road.

---

## 1. How to use this with an LLM

1. Give your model this file, `schema/strategy.schema.json`, and the files in `examples/`.
2. Tell it: *"Author one EABuilder strategy as a single JSON object that validates
   against the schema. Ask me about my idea first, then output only the JSON."*
3. Paste the JSON into EABuilder (or upload the file).
4. If validation fails, **paste the returned errors back to your model** — each error
   carries a `path`, a `problem`, and a `fix`, so the model can correct itself. Repeat
   until it validates.

The validator is strict on purpose. A spec that validates is one that will generate
safe, compilable code.

---

## 2. Document shape

```jsonc
{
  "specVersion": "1.0",
  "meta":   { ... },            // name, symbol, timeframe, when to evaluate
  "indicators": [ ... ],        // optional — declare reusable indicator operands
  "risk":   { ... },            // REQUIRED — always-on safety envelope
  "newsFilter": { ... },        // optional — avoid economic-calendar events
  "sessions":   { ... },        // optional — time-of-day / day-of-week windows
  "positionManagement": { ... },// optional — breakeven / trailing stop
  "rules":  [ ... ]             // REQUIRED — IF <when> THEN <actions>
}
```

`specVersion`, `meta`, `risk`, and `rules` are required. Everything else is optional.

---

## 3. Indicators → named operands

Each indicator you declare gets an `id` (lower_snake_case). That `id` becomes an
**operand** you can use in expressions and in ATR-based stops.

```json
{ "id": "ema_200", "type": "EMA", "params": { "period": 200, "appliedPrice": "close" } }
```

Referencing `ema_200` in an expression yields the indicator's **current value**.
Use a bar index to look back: `ema_200[1]` is the value on the previous closed bar.

Supported `type`s and their relevant `params`:

| type             | params                                              |
|------------------|-----------------------------------------------------|
| `SMA`, `EMA`     | `period`, `appliedPrice`                             |
| `RSI`            | `period`, `appliedPrice`                             |
| `ATR`            | `period`                                             |
| `MACD`           | `fastPeriod`, `slowPeriod`, `signalPeriod`, `appliedPrice` |
| `BollingerBands` | `period`, `deviations`, `appliedPrice`              |
| `Stochastic`     | `kPeriod`, `dPeriod`, `slowing`                     |
| `ADX`            | `period`                                            |

`appliedPrice`: `open` `high` `low` `close` `median` `typical` `weighted`.

---

## 4. The expression grammar (the `when` field)

Each rule's `when` is a **boolean expression**. It is parsed server-side into an AST
and validated against the declared operands — it is **never** compiled as a raw string.
Keep it to this grammar:

**Operators** (precedence high → low): `!`  →  `* /`  →  `+ -`  →  `< <= > >= == !=`  →  `&&`  →  `||`. Parentheses `( )` group.

**Functions** (the only ones allowed):

| function                | meaning                                                        |
|-------------------------|----------------------------------------------------------------|
| `crossesAbove(a, b)`    | `a` was ≤ `b` last bar and is > `b` now                         |
| `crossesBelow(a, b)`    | `a` was ≥ `b` last bar and is < `b` now                         |
| `abs(x)`                | absolute value                                                 |
| `min(a, b)` / `max(a, b)` | smaller / larger of two values                               |

**Operands** — identifiers resolving to a number, optionally bar-shifted with `[n]`
(`0` = current, `1` = previous closed bar, … up to `[100]`):

| operand                        | meaning                                          |
|--------------------------------|--------------------------------------------------|
| *any indicator `id`*           | that indicator's current value                   |
| `price.open/high/low/close`    | current bar OHLC                                 |
| `price.bid` / `price.ask`      | live bid / ask                                   |
| `spread`                       | current spread in points                         |
| `account.equity` / `account.balance` | account figures                            |
| `account.drawdownPct`          | current drawdown from peak, percent              |
| `positions.open`               | total open positions for this EA                 |
| `positions.long` / `positions.short` | open count per side                        |
| `time.hour` / `time.minute`    | server time (0–23 / 0–59)                        |
| `time.dayOfWeek`               | 1 = Mon … 7 = Sun                                |

Numeric literals (`30`, `1.5`) and booleans (`true`, `false`) are allowed.
Strings, assignment, loops, and any identifier not in the table above are **rejected**.

Examples:

```text
crossesAbove(ema_fast, ema_slow)
price.close > ema_200 && rsi_14 < 35 && rsi_14[1] >= 35
spread <= 20 && positions.open == 0 && time.hour >= 7
```

---

## 5. Rules — IF `when` THEN `actions`

```json
{
  "id": "long_entry",
  "when": "price.close > ema_200 && rsi_14 < 35",
  "cooldownBars": 4,
  "actions": [
    { "type": "openTrade", "side": "buy", "lot": 0.1,
      "sl": { "mode": "atr", "indicator": "atr_14", "multiplier": 1.5 },
      "tp": { "mode": "points", "value": 600 } },
    { "type": "alert", "channel": "telegram", "message": "Long {symbol} @ {price.ask}" }
  ]
}
```

Rules are evaluated in order, once per bar (or per tick if `meta.evaluateOn` is `tick`).
`cooldownBars` throttles re-firing.

### Action types

- **`openTrade`** — market order. `side` (`buy`/`sell`), `lot`, `sl`, `tp`, optional `tag`/`comment`.
- **`openPending`** — pending order. `side` (`buy`/`sell`), `pending` (`limit`/`stop`), `price`
  (the `placement`, see below), `lot`, `sl`, `tp`, optional `expireMinutes`, `tag`/`comment`.
- **`closeTrade`** — `target` (`all`/`long`/`short`/`tag`); with `tag` close only matching
  positions. Optional `amount` makes it a **partial** close (see below).
- **`cancelPending`** — delete pending orders. `target` (`all`/`buy`/`sell`/`tag`).
- **`alert`** — `channel` (`email`/`telegram`/`discord`/`all`) + `message`. Alerts are delivered
  by the EABuilder relay to **your own** linked email / Telegram chat / Discord channel (never
  to anyone else). Included with EAHosting; otherwise a 300 KES/month add-on.

### Pending order price (`price` — the `placement`)

The four combinations are buy-stop (breakout up), sell-stop (breakout down), buy-limit
(pullback), sell-limit (rally fade). `price` says where the order sits:

| mode     | fields                    | meaning                                                  |
|----------|---------------------------|----------------------------------------------------------|
| `price`  | `value`                   | absolute price level                                     |
| `points` | `value`                   | offset in points from current price (direction by type)  |
| `atr`    | `indicator`, `multiplier` | offset = ATR × multiplier from current price             |

`sl`/`tp` on a pending order are measured from the **pending entry price**, not the current
price. `expireMinutes` cancels the order if it hasn't filled in time (omit = good-till-cancelled).
Pending orders count toward `risk.maxOpenPositions` alongside open positions, so a rule can't
endlessly stack them.

### Partial closes (`amount` on `closeTrade`)

Omit `amount` for a full close. Otherwise:

| mode      | fields  | meaning                                              |
|-----------|---------|------------------------------------------------------|
| `percent` | `value` | close this % of each matching position's volume      |
| `lots`    | `value` | close this many lots from each matching position     |

Volumes are floored to the broker's lot step; if the result is below the minimum lot, that
position is left untouched (you can't partial-close below the minimum).

### Stops & take-profits (`sl` / `tp`)

| mode     | fields                          | meaning                                  |
|----------|---------------------------------|------------------------------------------|
| `points` | `value`                         | fixed distance in points                 |
| `price`  | `value`                         | absolute price level                     |
| `atr`    | `indicator`, `multiplier`       | distance = ATR × multiplier (volatility-scaled) |
| `none`   | —                               | no stop / no target                      |

### Lot sizing (`lot`)

- A bare number → fixed lots: `"lot": 0.1`.
- `{ "mode": "fixed", "value": 0.1 }` — same, explicit.
- `{ "mode": "riskPercent", "value": 1.0 }` — size so the SL distance risks 1% of equity.
  Requires an `sl` with a measurable distance (`points` or `atr`).

### Alert message placeholders

`{symbol}` `{price.bid}` `{price.ask}` `{price.close}` and any indicator `id` (e.g. `{rsi_14}`)
are substituted at send time. Telegram goes through the platform's bot; email through the
platform mailer — you don't configure transport here.

---

## 6. The risk envelope (`risk`) — required, and enforced

```json
"risk": {
  "maxLot": 0.5,
  "maxOpenPositions": 1,
  "maxDailyLossPct": 4,
  "maxSpreadPoints": 50,
  "slippagePoints": 15
}
```

These are **hard limits the generated EA enforces on every order** — and the platform
**clamps them again** to your plan/account ceiling. If you ask for `maxLot: 50` but your
plan caps at `2`, the EA runs with `2`. Treat this block as the safety floor under
everything your rules try to do; it cannot be disabled.

---

## 7. News & sessions

- **`newsFilter`** uses the platform's central economic calendar — you only declare the
  rule (`currencies`, `minImpact`, the block window, and whether to `no_new_trades` or
  `flatten`). The EA never scrapes anything itself.
- **`sessions`** restrict *new entries* to the given day/time windows. Exits and position
  management still run outside them.

---

## 8. What gets rejected (common LLM mistakes)

- Inventing indicator types or operands not listed here.
- Referencing an indicator `id` in an expression without declaring it in `indicators`.
- Raw MQL5, function calls other than the four whitelisted ones, or string operands.
- `riskPercent` lot sizing with `sl.mode: "none"` (no distance to size against).
- Omitting `risk` or `rules`, or an empty `rules` array.
- Extra/misspelled fields — the schema is `additionalProperties: false` everywhere.

When in doubt, copy the closest file in `examples/` and modify it.
