{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://eaforge.app/spec/v1/strategy.schema.json",
  "title": "EAForge Strategy Spec",
  "description": "Declarative description of an MT5 Expert Advisor. A code generator turns a valid instance of this schema into compilable .mq5 source. Safety-critical behaviour (orders, stops, risk) is expressed only through fixed blocks; signal logic is expressed through a sandboxed expression grammar (see SPEC.md). Anything not described here is not allowed.",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "specVersion",
    "meta",
    "risk",
    "rules"
  ],
  "properties": {
    "specVersion": {
      "description": "Spec format version. Must match a version the platform currently accepts.",
      "type": "string",
      "enum": [
        "1.0"
      ]
    },
    "meta": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "name",
        "symbol",
        "timeframe"
      ],
      "properties": {
        "name": {
          "type": "string",
          "minLength": 1,
          "maxLength": 80
        },
        "description": {
          "type": "string",
          "maxLength": 500
        },
        "symbol": {
          "description": "Default symbol the EA is designed for. The host may override at deploy time.",
          "type": "string",
          "pattern": "^[A-Z0-9._]{2,16}$"
        },
        "timeframe": {
          "type": "string",
          "enum": [
            "M1",
            "M5",
            "M15",
            "M30",
            "H1",
            "H4",
            "D1",
            "W1",
            "MN1"
          ]
        },
        "evaluateOn": {
          "description": "When rules are evaluated. 'barClose' (default) is deterministic and backtest-friendly; 'tick' fires every price change.",
          "type": "string",
          "enum": [
            "barClose",
            "tick"
          ],
          "default": "barClose"
        }
      }
    },
    "indicators": {
      "description": "Declared indicators. Each 'id' becomes a named operand usable in rule expressions and ATR-based stops.",
      "type": "array",
      "maxItems": 16,
      "items": {
        "$ref": "#/$defs/indicator"
      }
    },
    "risk": {
      "$ref": "#/$defs/risk"
    },
    "newsFilter": {
      "$ref": "#/$defs/newsFilter"
    },
    "sessions": {
      "$ref": "#/$defs/sessions"
    },
    "positionManagement": {
      "$ref": "#/$defs/positionManagement"
    },
    "rules": {
      "type": "array",
      "minItems": 1,
      "maxItems": 32,
      "items": {
        "$ref": "#/$defs/rule"
      }
    }
  },
  "$defs": {
    "indicatorId": {
      "type": "string",
      "pattern": "^[a-z][a-z0-9_]{1,30}$",
      "description": "lower_snake_case identifier, unique within the spec."
    },
    "appliedPrice": {
      "type": "string",
      "enum": [
        "open",
        "high",
        "low",
        "close",
        "median",
        "typical",
        "weighted"
      ]
    },
    "indicator": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "id",
        "type"
      ],
      "properties": {
        "id": {
          "$ref": "#/$defs/indicatorId"
        },
        "type": {
          "type": "string",
          "enum": [
            "SMA",
            "EMA",
            "RSI",
            "MACD",
            "ATR",
            "BollingerBands",
            "Stochastic",
            "ADX"
          ]
        },
        "params": {
          "type": "object",
          "additionalProperties": false,
          "properties": {
            "period": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000
            },
            "appliedPrice": {
              "$ref": "#/$defs/appliedPrice"
            },
            "fastPeriod": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000
            },
            "slowPeriod": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000
            },
            "signalPeriod": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000
            },
            "deviations": {
              "type": "number",
              "exclusiveMinimum": 0,
              "maximum": 10
            },
            "kPeriod": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000
            },
            "dPeriod": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000
            },
            "slowing": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000
            }
          }
        }
      }
    },
    "risk": {
      "description": "Always-on risk envelope. The generator enforces these in the EA AND the platform clamps any value to the user's plan/account ceiling — values here are an upper bound the user requests, never a guarantee they are honoured as-is.",
      "type": "object",
      "additionalProperties": false,
      "required": [
        "maxLot",
        "maxOpenPositions",
        "maxDailyLossPct"
      ],
      "properties": {
        "maxLot": {
          "type": "number",
          "exclusiveMinimum": 0,
          "maximum": 100
        },
        "maxOpenPositions": {
          "type": "integer",
          "minimum": 1,
          "maximum": 50
        },
        "maxDailyLossPct": {
          "type": "number",
          "exclusiveMinimum": 0,
          "maximum": 100
        },
        "maxSpreadPoints": {
          "type": "integer",
          "minimum": 0,
          "maximum": 100000
        },
        "slippagePoints": {
          "type": "integer",
          "minimum": 0,
          "maximum": 100000
        }
      }
    },
    "newsFilter": {
      "description": "Avoid trading around economic-calendar events. The calendar is supplied centrally by the platform; the EA only evaluates the rule.",
      "type": "object",
      "additionalProperties": false,
      "required": [
        "enabled"
      ],
      "properties": {
        "enabled": {
          "type": "boolean"
        },
        "currencies": {
          "type": "array",
          "items": {
            "type": "string",
            "pattern": "^[A-Z]{3}$"
          },
          "maxItems": 12
        },
        "minImpact": {
          "type": "string",
          "enum": [
            "low",
            "medium",
            "high"
          ],
          "default": "high"
        },
        "blockBeforeMin": {
          "type": "integer",
          "minimum": 0,
          "maximum": 1440,
          "default": 30
        },
        "blockAfterMin": {
          "type": "integer",
          "minimum": 0,
          "maximum": 1440,
          "default": 30
        },
        "action": {
          "description": "'no_new_trades' blocks new entries in the window; 'flatten' also closes open positions before the event.",
          "type": "string",
          "enum": [
            "no_new_trades",
            "flatten"
          ],
          "default": "no_new_trades"
        }
      }
    },
    "sessions": {
      "description": "Time-of-day / day-of-week trading windows. Outside any window, new entries are blocked.",
      "type": "object",
      "additionalProperties": false,
      "required": [
        "enabled"
      ],
      "properties": {
        "enabled": {
          "type": "boolean"
        },
        "timezone": {
          "type": "string",
          "default": "UTC"
        },
        "windows": {
          "type": "array",
          "minItems": 1,
          "maxItems": 12,
          "items": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "days",
              "from",
              "to"
            ],
            "properties": {
              "days": {
                "type": "array",
                "minItems": 1,
                "uniqueItems": true,
                "items": {
                  "type": "string",
                  "enum": [
                    "mon",
                    "tue",
                    "wed",
                    "thu",
                    "fri",
                    "sat",
                    "sun"
                  ]
                }
              },
              "from": {
                "type": "string",
                "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$"
              },
              "to": {
                "type": "string",
                "pattern": "^([01]\\d|2[0-3]):[0-5]\\d$"
              }
            }
          }
        }
      }
    },
    "positionManagement": {
      "description": "Per-tick management of open positions. Runs independently of rules.",
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "breakeven": {
          "type": "object",
          "additionalProperties": false,
          "required": [
            "enabled"
          ],
          "properties": {
            "enabled": {
              "type": "boolean"
            },
            "triggerPoints": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000000
            },
            "offsetPoints": {
              "type": "integer",
              "minimum": 0,
              "maximum": 1000000
            }
          }
        },
        "trailingStop": {
          "type": "object",
          "additionalProperties": false,
          "required": [
            "enabled",
            "mode"
          ],
          "properties": {
            "enabled": {
              "type": "boolean"
            },
            "mode": {
              "type": "string",
              "enum": [
                "points",
                "atr"
              ]
            },
            "distancePoints": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000000
            },
            "indicator": {
              "$ref": "#/$defs/indicatorId"
            },
            "multiplier": {
              "type": "number",
              "exclusiveMinimum": 0,
              "maximum": 50
            },
            "step": {
              "type": "integer",
              "minimum": 1,
              "maximum": 1000000,
              "default": 10
            }
          }
        }
      }
    },
    "sltp": {
      "description": "Stop-loss / take-profit specification.",
      "type": "object",
      "discriminator": {
        "propertyName": "mode"
      },
      "required": [
        "mode"
      ],
      "oneOf": [
        {
          "additionalProperties": false,
          "required": [
            "mode",
            "value"
          ],
          "properties": {
            "mode": {
              "const": "points"
            },
            "value": {
              "type": "number",
              "exclusiveMinimum": 0
            }
          }
        },
        {
          "additionalProperties": false,
          "required": [
            "mode",
            "value"
          ],
          "properties": {
            "mode": {
              "const": "price"
            },
            "value": {
              "type": "number",
              "exclusiveMinimum": 0
            }
          }
        },
        {
          "additionalProperties": false,
          "required": [
            "mode",
            "indicator",
            "multiplier"
          ],
          "properties": {
            "mode": {
              "const": "atr"
            },
            "indicator": {
              "$ref": "#/$defs/indicatorId"
            },
            "multiplier": {
              "type": "number",
              "exclusiveMinimum": 0,
              "maximum": 50
            }
          }
        },
        {
          "additionalProperties": false,
          "required": [
            "mode"
          ],
          "properties": {
            "mode": {
              "const": "none"
            }
          }
        }
      ]
    },
    "lot": {
      "description": "Position size. A bare number is shorthand for fixed lots; the platform clamps to risk.maxLot and broker min/step.",
      "oneOf": [
        {
          "type": "number",
          "exclusiveMinimum": 0,
          "maximum": 100
        },
        {
          "type": "object",
          "additionalProperties": false,
          "required": [
            "mode",
            "value"
          ],
          "properties": {
            "mode": {
              "const": "fixed"
            },
            "value": {
              "type": "number",
              "exclusiveMinimum": 0,
              "maximum": 100
            }
          }
        },
        {
          "type": "object",
          "additionalProperties": false,
          "required": [
            "mode",
            "value"
          ],
          "properties": {
            "mode": {
              "const": "riskPercent"
            },
            "value": {
              "type": "number",
              "exclusiveMinimum": 0,
              "maximum": 20
            },
            "comment": {
              "type": "string"
            }
          }
        }
      ]
    },
    "placement": {
      "description": "Where to place a pending order's entry price.",
      "type": "object",
      "discriminator": {
        "propertyName": "mode"
      },
      "required": [
        "mode"
      ],
      "oneOf": [
        {
          "additionalProperties": false,
          "required": [
            "mode",
            "value"
          ],
          "properties": {
            "mode": {
              "const": "price"
            },
            "value": {
              "type": "number",
              "exclusiveMinimum": 0
            }
          }
        },
        {
          "additionalProperties": false,
          "required": [
            "mode",
            "value"
          ],
          "properties": {
            "mode": {
              "const": "points"
            },
            "value": {
              "type": "number",
              "exclusiveMinimum": 0
            }
          }
        },
        {
          "additionalProperties": false,
          "required": [
            "mode",
            "indicator",
            "multiplier"
          ],
          "properties": {
            "mode": {
              "const": "atr"
            },
            "indicator": {
              "$ref": "#/$defs/indicatorId"
            },
            "multiplier": {
              "type": "number",
              "exclusiveMinimum": 0,
              "maximum": 50
            }
          }
        }
      ]
    },
    "amount": {
      "description": "How much of a matching position to close (omit for a full close).",
      "type": "object",
      "discriminator": {
        "propertyName": "mode"
      },
      "required": [
        "mode",
        "value"
      ],
      "oneOf": [
        {
          "additionalProperties": false,
          "required": [
            "mode",
            "value"
          ],
          "properties": {
            "mode": {
              "const": "percent"
            },
            "value": {
              "type": "number",
              "exclusiveMinimum": 0,
              "maximum": 100
            }
          }
        },
        {
          "additionalProperties": false,
          "required": [
            "mode",
            "value"
          ],
          "properties": {
            "mode": {
              "const": "lots"
            },
            "value": {
              "type": "number",
              "exclusiveMinimum": 0,
              "maximum": 100
            }
          }
        }
      ]
    },
    "action": {
      "type": "object",
      "discriminator": {
        "propertyName": "type"
      },
      "required": [
        "type"
      ],
      "oneOf": [
        {
          "additionalProperties": false,
          "required": [
            "type",
            "side"
          ],
          "properties": {
            "type": {
              "const": "openTrade"
            },
            "side": {
              "type": "string",
              "enum": [
                "buy",
                "sell"
              ]
            },
            "lot": {
              "$ref": "#/$defs/lot"
            },
            "sl": {
              "$ref": "#/$defs/sltp"
            },
            "tp": {
              "$ref": "#/$defs/sltp"
            },
            "tag": {
              "type": "string",
              "maxLength": 31,
              "pattern": "^[A-Za-z0-9_-]*$"
            },
            "comment": {
              "type": "string",
              "maxLength": 31
            }
          }
        },
        {
          "additionalProperties": false,
          "required": [
            "type",
            "side",
            "pending",
            "price"
          ],
          "properties": {
            "type": {
              "const": "openPending"
            },
            "side": {
              "type": "string",
              "enum": [
                "buy",
                "sell"
              ]
            },
            "pending": {
              "type": "string",
              "enum": [
                "limit",
                "stop"
              ]
            },
            "price": {
              "$ref": "#/$defs/placement"
            },
            "lot": {
              "$ref": "#/$defs/lot"
            },
            "sl": {
              "$ref": "#/$defs/sltp"
            },
            "tp": {
              "$ref": "#/$defs/sltp"
            },
            "expireMinutes": {
              "description": "Cancel the pending order this many minutes after placement (omit = good-till-cancelled).",
              "type": "integer",
              "minimum": 1,
              "maximum": 40320
            },
            "tag": {
              "type": "string",
              "maxLength": 31,
              "pattern": "^[A-Za-z0-9_-]*$"
            },
            "comment": {
              "type": "string",
              "maxLength": 31
            }
          }
        },
        {
          "additionalProperties": false,
          "required": [
            "type",
            "target"
          ],
          "properties": {
            "type": {
              "const": "closeTrade"
            },
            "target": {
              "type": "string",
              "enum": [
                "all",
                "long",
                "short",
                "tag"
              ]
            },
            "tag": {
              "type": "string",
              "maxLength": 31,
              "pattern": "^[A-Za-z0-9_-]*$"
            },
            "amount": {
              "$ref": "#/$defs/amount"
            }
          }
        },
        {
          "additionalProperties": false,
          "required": [
            "type",
            "target"
          ],
          "properties": {
            "type": {
              "const": "cancelPending"
            },
            "target": {
              "type": "string",
              "enum": [
                "all",
                "buy",
                "sell",
                "tag"
              ]
            },
            "tag": {
              "type": "string",
              "maxLength": 31,
              "pattern": "^[A-Za-z0-9_-]*$"
            }
          }
        },
        {
          "additionalProperties": false,
          "required": [
            "type",
            "channel",
            "message"
          ],
          "properties": {
            "type": {
              "const": "alert"
            },
            "channel": {
              "type": "string",
              "enum": [
                "email",
                "telegram",
                "discord",
                "all"
              ]
            },
            "message": {
              "type": "string",
              "minLength": 1,
              "maxLength": 500
            }
          }
        }
      ]
    },
    "rule": {
      "type": "object",
      "additionalProperties": false,
      "required": [
        "id",
        "when",
        "actions"
      ],
      "properties": {
        "id": {
          "type": "string",
          "pattern": "^[a-z][a-z0-9_]{1,40}$"
        },
        "when": {
          "description": "Boolean expression over named operands (see SPEC.md). Parsed and validated server-side into an AST; never compiled as a raw string.",
          "type": "string",
          "minLength": 1,
          "maxLength": 500
        },
        "actions": {
          "type": "array",
          "minItems": 1,
          "maxItems": 8,
          "items": {
            "$ref": "#/$defs/action"
          }
        },
        "cooldownBars": {
          "description": "Minimum bars between firings of this rule.",
          "type": "integer",
          "minimum": 0,
          "maximum": 100000,
          "default": 0
        }
      }
    }
  }
}