EmidLabs Backtest

Run crypto trading strategy backtests through EmidLabs's Backtesting API.

Should I use this

Quality & Safety

A
Description quality
100%
Schema completeness
90%
Naming quality
98%
Poisoning risk
80%
Permission match
100%
Protocol compliance
100%

Findings (3)

  • HIGHTool poisoning patterns detected
  • MEDIUMTool description contains suspicious base64-like encoded stringin get_backtest_batch_results
  • LOWTool 'get_confirmation_source_signals' name length outside 3-30 rangein get_confirmation_source_signals

Based on automated analysis of tool definitions and protocol compliance.

Context Cost

~12,605Tokens (tool definitions)
~15.6 KBTypical response size
Significant attention impact (9.85% of 128k context)

This is the approximate number of tokens consumed each time the server's tools are loaded into a model's context. Higher counts reduce the attention available for other tasks.

Install

One-Click Install

Add this to your `claude_desktop_config.json` file:

{
  "mcpServers": {
    "backtest-mcp": {
      "url": "https://mcp.backtest.emidlabs.com/mcp"
    }
  }
}

Remote endpoints

https://mcp.backtest.emidlabs.com/mcpstreamable-http

What it can do

Tool inventory

Tools (9)

🟢 Read-only🟡 Write🔴 Delete⚪ Unknown
🟡submit_confirmation_source(assetPair, initialDate, finalDate, strategySnapshotJson, backtestApiKey, ...)

Runs a strategy against historical data purely to produce a signal timeline used to confirm OTHER backtests — not a backtest itself. Walks the same candles and evaluates the same DSL as submit_backtest, but never simulates a position: no trade, no PnL, no WinRate/drawdown, none of that applies here, because this strategy is never meant to be traded on its own. Returns immediately with an id and status — call get_confirmation_source to check completion, then pass that id as a confirmationSources sourceId in submit_backtest (e.g. only count an XRP entry once a BTC-neutral confirmation source agrees). Costs the same capacity as a regular backtest of the same size — the compute is identical, it just skips trade simulation.

Input Schema

{
  "type": "object",
  "properties": {
    "assetPair": {
      "description": "e.g. \"BTC-USDC\".",
      "type": [
        "string",
        "null"
      ],
      "default": null
    },
    "initialDate": {
      "description": "ISO date string, e.g. \"2025-01-01\".",
      "type": [
        "string",
        "null"
      ],
      "default": null
    },
    "finalDate": {
      "description": "ISO date string, e.g. \"2025-06-01\".",
      "type": [
        "string",
        "null"
      ],
      "default": null
    },
    "strategySnapshotJson": {
      "description": "The Strategy DSL object — identical shape to submit_backtest's own. riskManagement is accepted but never used (no position is ever opened), so it's fine to omit.",
      "type": [
        "object",
        "null"
      ],
      "properties": {
        "configuration": {
          "type": [
            "object",
            "null"
          ],
          "properties": {
            "timeframe": {
              "description": "One of \"5M\", \"15M\", \"30M\", \"1H\", \"2H\", \"4H\", \"1D\".",
              "type": [
                "string",
                "null"
              ]
            },
            "warmupBars": {
              "description": "Leading candles excluded from trading while indicators stabilize. Default 0.",
              "type": "integer"
            },
            "maxOpenPositions": {
              "description": "Caps concurrent open trades. Omit/null for unlimited (default).",
              "type": [
                "integer",
                "null"
              ]
            },
            "direction": {
              "description": "\"long\" or \"short\" — picks which side the WHOLE backtest trades, not a mix. Only affects stop-loss/take-profit price placement and the profit/loss sign (mirrored for short: stop above entry, take below entry, profit when price falls) — conditions/score/decision keep the exact same meaning either way. Defaults to \"long\".",
              "type": [
                "string",
                "null"
              ]
            },
            "entryFeePct": {
              "description": "Simulated exchange fee on entry, as a percent (e.g. 0.1 = 0.1%). Omit/null = 0 (no fee, backward compatible). Subtracted from every trade's pnlR/pnlPct so results are net-of-fee by construction. 0-5 range. See the pnlR/expectancyR field descriptions on get_backtest_result for the R-vs-% caveat once a fee is set.",
              "type": [
                "number",
                "null"
              ]
            },
            "exitFeePct": {
              "description": "Simulated exchange fee on exit, as a percent (e.g. 0.1 = 0.1%). Same semantics as EntryFeePct — real exchanges can charge different maker/taker rates per leg, so this is independent, not assumed equal.",
              "type": [
                "number",
                "null"
              ]
            },
            "timezone": {
              "description": "IANA timezone id (e.g. \"America/Sao_Paulo\") used to localize the DSL's hour()/minute()/dayOfWeek()/isWeekend() functions, for time-of-day or weekday gating conditions. Omit/null defaults to \"America/Sao_Paulo\".",
              "type": [
                "string",
                "null"
              ]
            }
          }
        },
        "inputs": {
          "description": "Named computed values, one JSON-string expression each, evaluated once per candle. Raw fields: close, open, high, low, volume. Built-in functions (case-sensitive, lowercase — no others exist, e.g. avg()/SMA() uppercase/close[N] bracket indexing are NOT supported): ema(series,period), sma(series,period), rsi(series,period), atr(period), adx(period), adxPlusDi(period), adxMinusDi(period), crossUp(a,b), crossDown(a,b), highest(series,n), lowest(series,n), change(series), shift(series,n) [look-back only, NaN before enough history], any(boolSeries,n), all(boolSeries,n), count(boolSeries,n) [over the n candles before the current one], swingHigh(series,confirmBars), swingLow(series,confirmBars) [confirmed N-bar swing point, true confirmBars candles AFTER the actual peak/trough — never at the peak itself, so it can't repaint between backtest and live], body(), range(), upperWick(), lowerWick(), isBullish(), isBearish(), abs(x), min(a,b), max(a,b). There is no volumeSma()/volumeSpike() — volume is a plain series like close/open/high/low, so use sma(volume,period) and volume > sma(volume,period) * multiplier instead. Candlestick patterns (all no-arg, boolean, read only closed-candle OHLC, same in backtest and live): single-candle hammer(), shootingStar(), doji(), bullishMarubozu(), bearishMarubozu(), spinningTop(), dragonflyDoji(), gravestoneDoji(), longLeggedDoji(); two-candle bullishEngulfing(), bearishEngulfing(), piercingLine(), darkCloudCover(), bullishHarami(), bearishHarami(), haramiCross(), tweezerTop(), tweezerBottom(); three-plus-candle morningStar(), eveningStar(), threeWhiteSoldiers(), threeBlackCrows(), threeInsideUp(), threeInsideDown(), threeOutsideUp(), threeOutsideDown(), risingThreeMethods(), fallingThreeMethods(). Caveat: hammer()/shootingStar() are shape-only, no prior-trend check (same shape is Hammer in a downtrend but Hanging Man in an uptrend, and vice versa for shootingStar/Inverted Hammer) — pair with a trend/momentum condition rather than using the shape alone. An input may only reference inputs defined above it (no forward/circular references). Example: {\"emaFast\":\"ema(close, 9)\",\"emaSlow\":\"ema(close, 21)\"}",
          "type": [
            "object",
            "null"
          ],
          "additionalProperties": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "conditions": {
          "description": "Named boolean expressions over inputs/market data. Operators: > < >= <= == != AND OR. Example: {\"trendUp\":\"emaFast > emaSlow AND adx14 > 20\"}",
          "type": [
            "object",
            "null"
          ],
          "additionalProperties": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "score": {
          "description": "Integer weight per condition name (keys must match \"conditions\"). The engine sums the weights of every true condition on a candle; compare the total against a threshold in \"decision.entry\" (e.g. \"score >= 50\").",
          "type": [
            "object",
            "null"
          ],
          "additionalProperties": {
            "type": "integer"
          }
        },
        "decision": {
          "type": [
            "object",
            "null"
          ],
          "properties": {
            "entry": {
              "description": "Boolean expression evaluated per candle; a trade opens when true.",
              "type": [
                "string",
                "null"
              ]
            },
            "exit": {
              "description": "Optional boolean expression; closes an open position when true, independent of riskManagement. If a candle's stop-loss/take-profit and \"exit\" would both trigger, the stop-loss/take-profit wins and exit is not evaluated for that position that candle. If omitted, positions only close via stop-loss/take-profit.",
              "type": [
                "string",
                "null"
              ]
            }
          }
        },
        "riskManagement": {
          "description": "Configures stop-loss/take-profit. Omit entirely for the default (1% stop-loss, 1:3 risk-reward take-profit) — see stopLoss/takeProfit below for the full shape of each.",
          "type": [
            "object",
            "null"
          ],
          "properties": {
            "stopLoss": {
              "description": "Stop-loss policy. Exactly one of: {\"type\":\"percent\",\"percent\":<number, e.g. 1.0 for 1%>} or {\"type\":\"atr\",\"period\":<int>,\"multiplier\":<number>}. \"type\" is required; field order does not matter."
            },
            "takeProfit": {
              "description": "Take-profit policy. Exactly one of: {\"type\":\"riskReward\",\"multiple\":<number>} (distance = stop-loss distance * multiple) or {\"type\":\"percent\",\"percent\":<number>}. \"type\" is required; field order does not matter."
            }
          }
        }
      },
      "default": null
    },
    "backtestApiKey": {
      "description": "Your EmidLabs backtest API key (created in the Console). Not needed if this connector was added with a static 'x-api-key' header.",
      "type": [
        "string",
        "null"
      ],
      "default": null
    },
    "backtestBaseUrl": {
      "description": "Defaults to the public production API. Override only for self-hosted/staging use.",
      "type": [
        "string",
        "null"
      ],
      "default": null
    }
  }
}

Output Schema

{
  "type": "object",
  "properties": {
    "id": {
      "type": "string"
    },
    "status": {
      "type": "string"
    }
  }
}
🟢list_available_assets(backtestApiKey, backtestBaseUrl)

Lists every asset pair with real historical data, each with its supported timeframes and the date range actually available. Optional to call — submit_backtest already returns a clear error (with the real available range) when an asset or date range doesn't have data, so this is for up-front exploration or for recovering from an "unknown asset" error, not a required step before every submit_backtest.

Input Schema

{
  "type": "object",
  "properties": {
    "backtestApiKey": {
      "description": "Your EmidLabs backtest API key. Not needed if this connector was added with a static 'x-api-key' header.",
      "type": [
        "string",
        "null"
      ],
      "default": null
    },
    "backtestBaseUrl": {
      "description": "Defaults to the public production API. Override only for self-hosted/staging use.",
      "type": [
        "string",
        "null"
      ],
      "default": null
    }
  }
}

Output Schema

{
  "type": "object",
  "properties": {
    "assets": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "assetPair": {
            "type": "string"
          },
          "timeframes": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "firstAvailable": {
            "type": "string",
            "format": "date-time"
          },
          "lastAvailable": {
            "type": "string",
            "format": "date-time"
          }
        },
        "required": [
          "assetPair"
        ]
      }
    }
  }
}
🟡submit_backtest_batch(assetPairs, initialDate, finalDate, strategySnapshotJson, confirmationSources, ...)

Submits ONE strategy against MANY asset pairs in a single call — the same backtest you'd get from calling submit_backtest once per asset, without needing dozens of round trips. Returns immediately with a batchId (pass it to get_backtest_batch_results to fetch every asset's outcome, paginated) plus the per-asset id/status assigned right away. Best-effort: one bad asset pair (unknown, or its date range outside coverage) shows up with an error in that item only — every other asset in the batch is unaffected. Optionally accepts confirmationSources, applied identically to every asset in the batch — see submit_backtest's own confirmationSources argument for the full semantics.

Input Schema

{
  "type": "object",
  "properties": {
    "assetPairs": {
      "description": "e.g. [\"BTC-USDC\", \"ETH-USDC\", \"SOL-USDC\"]. Every asset gets the exact same strategySnapshotJson and date range.",
      "type": [
        "array",
        "null"
      ],
      "items": {
        "type": [
          "string",
          "null"
        ]
      },
      "default": null
    },
    "initialDate": {
      "description": "ISO date string, e.g. \"2025-01-01\".",
      "type": [
        "string",
        "null"
      ],
      "default": null
    },
    "finalDate": {
      "description": "ISO date string, e.g. \"2025-06-01\".",
      "type": [
        "string",
        "null"
      ],
      "default": null
    },
    "strategySnapshotJson": {
      "description": "Same Strategy DSL object submit_backtest takes — see that tool's description for the full shape.",
      "type": [
        "object",
        "null"
      ],
      "properties": {
        "configuration": {
          "type": [
            "object",
            "null"
          ],
          "properties": {
            "timeframe": {
              "description": "One of \"5M\", \"15M\", \"30M\", \"1H\", \"2H\", \"4H\", \"1D\".",
              "type": [
                "string",
                "null"
              ]
            },
            "warmupBars": {
              "description": "Leading candles excluded from trading while indicators stabilize. Default 0.",
              "type": "integer"
            },
            "maxOpenPositions": {
              "description": "Caps concurrent open trades. Omit/null for unlimited (default).",
              "type": [
                "integer",
                "null"
              ]
            },
            "direction": {
              "description": "\"long\" or \"short\" — picks which side the WHOLE backtest trades, not a mix. Only affects stop-loss/take-profit price placement and the profit/loss sign (mirrored for short: stop above entry, take below entry, profit when price falls) — conditions/score/decision keep the exact same meaning either way. Defaults to \"long\".",
              "type": [
                "string",
                "null"
              ]
            },
            "entryFeePct": {
              "description": "Simulated exchange fee on entry, as a percent (e.g. 0.1 = 0.1%). Omit/null = 0 (no fee, backward compatible). Subtracted from every trade's pnlR/pnlPct so results are net-of-fee by construction. 0-5 range. See the pnlR/expectancyR field descriptions on get_backtest_result for the R-vs-% caveat once a fee is set.",
              "type": [
                "number",
                "null"
              ]
            },
            "exitFeePct": {
              "description": "Simulated exchange fee on exit, as a percent (e.g. 0.1 = 0.1%). Same semantics as EntryFeePct — real exchanges can charge different maker/taker rates per leg, so this is independent, not assumed equal.",
              "type": [
                "number",
                "null"
              ]
            },
            "timezone": {
              "description": "IANA timezone id (e.g. \"America/Sao_Paulo\") used to localize the DSL's hour()/minute()/dayOfWeek()/isWeekend() functions, for time-of-day or weekday gating conditions. Omit/null defaults to \"America/Sao_Paulo\".",
              "type": [
                "string",
                "null"
              ]
            }
          }
        },
        "inputs": {
          "description": "Named computed values, one JSON-string expression each, evaluated once per candle. Raw fields: close, open, high, low, volume. Built-in functions (case-sensitive, lowercase — no others exist, e.g. avg()/SMA() uppercase/close[N] bracket indexing are NOT supported): ema(series,period), sma(series,period), rsi(series,period), atr(period), adx(period), adxPlusDi(period), adxMinusDi(period), crossUp(a,b), crossDown(a,b), highest(series,n), lowest(series,n), change(series), shift(series,n) [look-back only, NaN before enough history], any(boolSeries,n), all(boolSeries,n), count(boolSeries,n) [over the n candles before the current one], swingHigh(series,confirmBars), swingLow(series,confirmBars) [confirmed N-bar swing point, true confirmBars candles AFTER the actual peak/trough — never at the peak itself, so it can't repaint between backtest and live], body(), range(), upperWick(), lowerWick(), isBullish(), isBearish(), abs(x), min(a,b), max(a,b). There is no volumeSma()/volumeSpike() — volume is a plain series like close/open/high/low, so use sma(volume,period) and volume > sma(volume,period) * multiplier instead. Candlestick patterns (all no-arg, boolean, read only closed-candle OHLC, same in backtest and live): single-candle hammer(), shootingStar(), doji(), bullishMarubozu(), bearishMarubozu(), spinningTop(), dragonflyDoji(), gravestoneDoji(), longLeggedDoji(); two-candle bullishEngulfing(), bearishEngulfing(), piercingLine(), darkCloudCover(), bullishHarami(), bearishHarami(), haramiCross(), tweezerTop(), tweezerBottom(); three-plus-candle morningStar(), eveningStar(), threeWhiteSoldiers(), threeBlackCrows(), threeInsideUp(), threeInsideDown(), threeOutsideUp(), threeOutsideDown(), risingThreeMethods(), fallingThreeMethods(). Caveat: hammer()/shootingStar() are shape-only, no prior-trend check (same shape is Hammer in a downtrend but Hanging Man in an uptrend, and vice versa for shootingStar/Inverted Hammer) — pair with a trend/momentum condition rather than using the shape alone. An input may only reference inputs defined above it (no forward/circular references). Example: {\"emaFast\":\"ema(close, 9)\",\"emaSlow\":\"ema(close, 21)\"}",
          "type": [
            "object",
            "null"
          ],
          "additionalProperties": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "conditions": {
          "description": "Named boolean expressions over inputs/market data. Operators: > < >= <= == != AND OR. Example: {\"trendUp\":\"emaFast > emaSlow AND adx14 > 20\"}",
          "type": [
            "object",
            "null"
          ],
          "additionalProperties": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "score": {
          "description": "Integer weight per condition name (keys must match \"conditions\"). The engine sums the weights of every true condition on a candle; compare the total against a threshold in \"decision.entry\" (e.g. \"score >= 50\").",
          "type": [
            "object",
            "null"
          ],
          "additionalProperties": {
            "type": "integer"
          }
        },
        "decision": {
          "type": [
            "object",
            "null"
          ],
          "properties": {
            "entry": {
              "description": "Boolean expression evaluated per candle; a trade opens when true.",
              "type": [
                "string",
                "null"
              ]
            },
            "exit": {
              "description": "Optional boolean expression; closes an open position when true, independent of riskManagement. If a candle's stop-loss/take-profit and \"exit\" would both trigger, the stop-loss/take-profit wins and exit is not evaluated for that position that candle. If omitted, positions only close via stop-loss/take-profit.",
              "type": [
                "string",
                "null"
              ]
            }
          }
        },
        "riskManagement": {
          "description": "Configures stop-loss/take-profit. Omit entirely for the default (1% stop-loss, 1:3 risk-reward take-profit) — see stopLoss/takeProfit below for the full shape of each.",
          "type": [
            "object",
            "null"
          ],
          "properties": {
            "stopLoss": {
              "description": "Stop-loss policy. Exactly one of: {\"type\":\"percent\",\"percent\":<number, e.g. 1.0 for 1%>} or {\"type\":\"atr\",\"period\":<int>,\"multiplier\":<number>}. \"type\" is required; field order does not matter."
            },
            "takeProfit": {
              "description": "Take-profit policy. Exactly one of: {\"type\":\"riskReward\",\"multiple\":<number>} (distance = stop-loss distance * multiple) or {\"type\":\"percent\",\"percent\":<number>}. \"type\" is required; field order does not matter."
            }
          }
        }
      },
      "default": null
    },
    "confirmationSources": {
      "description": "Optional. A JSON-encoded STRING (not a native array/object — pass it exactly like a quoted string value), containing the same shape as submit_backtest's own confirmationSources: [{\"sourceId\":\"<a submit_confirmation_source id>\",\"signalType\":\"entry\",\"validityWindow\":{\"count\":1}}]. Applied identically to every asset pair in this batch (one shared gate, not one per asset). Passed as a raw JSON string rather than a native array because this tool already has one array parameter (assetPairs) — a second one crashes the MCP SDK's own parameter marshaller. A sourceId that doesn't exist, isn't Completed, or belongs to another account fails that item only (same best-effort semantics as an unknown asset pair), not the whole batch.",
      "type": [
        "string",
        "null"
      ],
      "default": null
    },
    "backtestApiKey": {
      "description": "Your EmidLabs backtest API key (created in the Console). Not needed if this connector was added with a static 'x-api-key' header.",
      "type": [
        "string",
        "null"
      ],
      "default": null
    },
    "backtestBaseUrl": {
      "description": "Defaults to the public production API. Override only for self-hosted/staging use.",
      "type": [
        "string",
        "null"
      ],
      "default": null
    }
  }
}

Output Schema

{
  "type": "object",
  "properties": {
    "batchId": {
      "description": "Pass this to get_backtest_batch_results to fetch every item's status/result, paginated.",
      "type": "string"
    },
    "items": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "assetPair": {
            "type": "string"
          },
          "id": {
            "description": "Null if this asset pair failed at submission (see Error) — never appears in get_backtest_batch_results either, since no backtest record was ever created for it.",
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": [
              "string",
              "null"
            ]
          },
          "error": {
            "description": "Set only if this asset pair failed at submission (e.g. unknown asset pair, date range outside coverage). The other items in the batch are unaffected.",
            "type": [
              "string",
              "null"
            ]
          }
        }
      }
    }
  }
}
🟢get_backtest_batch_results(batchId, page, pageSize, waitForCompletion, pollTimeoutMs, ...)

Fetches every asset's outcome from a submit_backtest_batch call, paginated. By default (waitForCompletion: true) polls internally until every item in the batch is done — a cheap check, not one that pages through everything — so one call returns the finished page. Each item has the same aggregate metrics and recency fields (recentTradeCount/recentAvgPnlR/recentOutcomes) get_backtest_result returns for a single backtest, plus assetPair/status/error, so the single-result and batch-result shapes never drift apart. totalCount/completedCount/failedCount/pendingCount come back on every page, not just the last, so you can tell the batch is done from a single pageSize=1 call.

Input Schema

{
  "type": "object",
  "properties": {
    "batchId": {
      "description": "The batchId returned by submit_backtest_batch.",
      "type": [
        "string",
        "null"
      ],
      "default": null
    },
    "page": {
      "description": "1-based page number. Default 1.",
      "type": "integer",
      "default": 1
    },
    "pageSize": {
      "description": "Items per page, 1-100. Default 20.",
      "type": "integer",
      "default": 20
    },
    "waitForCompletion": {
      "description": "If true (default), polls internally until every item in the batch is done or pollTimeoutMs elapses.",
      "type": "boolean",
      "default": true
    },
    "pollTimeoutMs": {
      "description": "Defaults to 300000 (5 minutes) — a batch's slowest item determines the total wait, so this is higher than get_backtest_result's default.",
      "type": "integer",
      "default": 300000
    },
    "backtestApiKey": {
      "description": "Your EmidLabs backtest API key. Not needed if this connector was added with a static 'x-api-key' header.",
      "type": [
        "string",
        "null"
      ],
      "default": null
    },
    "backtestBaseUrl": {
      "description": "Defaults to the public production API.",
      "type": [
        "string",
        "null"
      ],
      "default": null
    }
  }
}

Output Schema

{
  "type": "object",
  "properties": {
    "batchId": {
      "type": "string"
    },
    "totalCount": {
      "description": "Total items in this batch, across every page — not just this one.",
      "type": "integer"
    },
    "completedCount": {
      "description": "How many items finished successfully.",
      "type": "integer"
    },
    "failedCount": {
      "description": "How many items finished unsuccessfully (Failed, Cancelled, or Expired).",
      "type": "integer"
    },
    "pendingCount": {
      "description": "How many items are still Queued or Running. The whole batch is done once this reaches 0 — true on every page, not just the last.",
      "type": "integer"
    },
    "page": {
      "type": "integer"
    },
    "pageSize": {
      "type": "integer"
    },
    "totalPages": {
      "type": "integer"
    },
    "items": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "assetPair": {
            "type": "string"
          },
          "id": {
            "description": "Never null here — an asset pair that failed at submission never became a real backtest record, so it never appears in this list. Check the original submit_backtest_batch response for submission-time failures.",
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "result": {
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "direction": {
                "description": "\"long\" or \"short\" — the direction the whole backtest traded.",
                "type": "string"
              },
              "pnlR": {
                "description": "Net profit/loss in R-units (risk multiples) — sum of every trade's PnL. This is the headline result.",
                "type": "number"
              },
              "trades": {
                "description": "Total number of closed trades.",
                "type": "integer"
              },
              "winRate": {
                "description": "wins / trades. Range 0–1.",
                "type": "number"
              },
              "wins": {
                "type": "integer"
              },
              "losses": {
                "type": "integer"
              },
              "grossProfitR": {
                "description": "Sum of all winning trades, in R-units.",
                "type": "number"
              },
              "grossLossR": {
                "description": "Sum of all losing trades, in R-units (negative).",
                "type": "number"
              },
              "profitFactor": {
                "description": "grossProfitR / abs(grossLossR). Greater than 1 means profitable. Null when there are no losing trades — the ratio is undefined (division by zero), not infinite.",
                "type": [
                  "number",
                  "null"
                ]
              },
              "avgWinR": {
                "description": "Average R on winning trades.",
                "type": "number"
              },
              "avgLossR": {
                "description": "Average R on losing trades (-1.0 with the default riskManagement; varies if the strategy overrides riskManagement.stopLoss).",
                "type": "number"
              },
              "expectancyR": {
                "description": "Average expected R per trade — (winRate * avgWinR) + (lossRate * avgLossR). This is the metric to optimize a strategy on, not raw winRate or trade count: a low win rate with a high avgWinR/avgLossR ratio can still have a strongly positive expectancyR.",
                "type": "number"
              },
              "totalFeeR": {
                "description": "Total R subtracted across all trades by configuration.entryFeePct/exitFeePct (0 if neither was set on the request). expectancyR/pnlR above are already net of this — TotalFeeR is just how much fees cost, for diagnostics. Caveat: a trade's fee-in-R cost scales inversely with that trade's own stop distance, so once fees are applied, expectancyR is only a fair comparison WITHIN one archetype's own stop convention — use pnlPct-based metrics (see get_backtest_trades) for comparisons across strategies/timeframes with different typical stop widths.",
                "type": "number"
              },
              "bothHit": {
                "description": "Trades where both stop-loss and take-profit were hit on the same candle (resolved as stop-loss). A high BothHit relative to Trades means many trades' outcome was decided by the engine's stop-wins-ties precedence rule rather than real intracandle price path data — treat results with more skepticism the higher this ratio is.",
                "type": "integer"
              },
              "maxDrawdownR": {
                "description": "Worst peak-to-trough dip across closed trades, in R-units. 0 if equity never fell below its running high-water mark. Historical/all-time — see CurrentDrawdownR for where the equity curve sits right now.",
                "type": "number"
              },
              "currentDrawdownR": {
                "description": "How far below its own peak the equity curve sits at the end of the backtest window, in R-units. 0 if the backtest ends at a new high. Includes any still-open position's unrealized PnL (see UnrealizedPnlRAtEnd) — a slump caused by an open, underwater position at window end shows up here.",
                "type": "number"
              },
              "openPositionsAtEnd": {
                "description": "Number of positions still open (never hit stop/take/exit-signal) when the backtest's date range ended. 0 in the common case. Check this before trusting CurrentDrawdownR/UnrealizedPnlRAtEnd at face value.",
                "type": "integer"
              },
              "unrealizedPnlRAtEnd": {
                "description": "Sum of unrealized PnL, in R-units, across all positions still open at window end — marked to market against the last available candle's close. 0 when OpenPositionsAtEnd is 0. Does NOT include exit fee (the position hasn't closed, so none has been paid) — a slight overestimate of true current drawdown when a position is open. This value feeds ONLY CurrentDrawdownR/MaxDrawdownR — it is never included in PnlR/ExpectancyR/Trades/Wins/Losses or any other metric describing closed, realized trades.",
                "type": "number"
              },
              "conditionsDistributionPct": {
                "description": "For each possible count of simultaneously-true conditions (0, 1, 2...N): fraction of all candles where exactly that many were true at once. A strategy-tuning diagnostic (how selective is the entry setup), not a performance metric.",
                "type": "object",
                "additionalProperties": {
                  "type": "number"
                }
              },
              "scoreDistributionPct": {
                "description": "For each possible total score value: fraction of candles that summed to it.",
                "type": "object",
                "additionalProperties": {
                  "type": "number"
                }
              },
              "confirmedSignalsCount": {
                "description": "Only meaningful when the request declared confirmationSources — how many candidates cleared confirmation and became one of the Trades above.",
                "type": "integer"
              },
              "unconfirmedSignalsCount": {
                "description": "Only meaningful when the request declared confirmationSources — how many candidates were dropped before trade simulation because they weren't corroborated. ConfirmedSignalsCount + UnconfirmedSignalsCount is the total candidate count, same as what a backtest without confirmationSources would have produced.",
                "type": "integer"
              }
            }
          },
          "runtimeMs": {
            "description": "How long the analyser actually took to run this item, in milliseconds — pure compute time. Null until Status is \"Completed\".",
            "type": [
              "integer",
              "null"
            ]
          },
          "candlesProcessed": {
            "description": "Number of candles the analyser processed for this item. Null until Status is \"Completed\". This is raw volume, not the plan's billing unit — see UnitsConsumed for that.",
            "type": [
              "integer",
              "null"
            ]
          },
          "unitsConsumed": {
            "description": "The actual execution-unit cost of this item, matching what's debited from the account's plan balance (CandlesProcessed normalized by the account's candles-per-unit rate). Null until Status is \"Completed\".",
            "type": [
              "number",
              "null"
            ]
          },
          "recentTradeCount": {
            "description": "Number of trades RecentAvgPnlR/RecentOutcomes are based on (up to 5). Null until Status is \"Completed\".",
            "type": [
              "integer",
              "null"
            ]
          },
          "recentAvgPnlR": {
            "description": "Average pnlR of the most recent RecentTradeCount closed trades.",
            "type": [
              "number",
              "null"
            ]
          },
          "recentOutcomes": {
            "description": "\"Win\"/\"Loss\" per recent trade, chronological — oldest first, last element is the most recent trade.",
            "type": [
              "array",
              "null"
            ],
            "items": {
              "type": "string"
            }
          }
        }
      }
    }
  }
}
🟢get_backtest_trades(id, page, pageSize, sortBy, sortDirection, ...)

Fetches the closed-trade list for a completed backtest, paginated and sortable — the trade-by-trade detail get_backtest_result deliberately omits. Only closed trades ever appear; a position still open when the backtest's date range ends isn't represented here or anywhere else. Example: sortBy="exitTime", sortDirection="desc", pageSize=5 for the most recently closed trades.

Input Schema

{
  "type": "object",
  "properties": {
    "id": {
      "description": "The id returned by submit_backtest.",
      "type": [
        "string",
        "null"
      ],
      "default": null
    },
    "page": {
      "description": "1-based page number. Default 1.",
      "type": "integer",
      "default": 1
    },
    "pageSize": {
      "description": "Trades per page, 1-100. Default 20.",
      "type": "integer",
      "default": 20
    },
    "sortBy": {
      "description": "One of: number, pnlR, pnlPct, entryTime, exitTime. Defaults to number (closing order).",
      "type": [
        "string",
        "null"
      ],
      "default": null
    },
    "sortDirection": {
      "description": "\"asc\" or \"desc\". Defaults to asc.",
      "type": [
        "string",
        "null"
      ],
      "default": null
    },
    "backtestApiKey": {
      "description": "Your EmidLabs backtest API key. Not needed if this connector was added with a static 'x-api-key' header.",
      "type": [
        "string",
        "null"
      ],
      "default": null
    },
    "backtestBaseUrl": {
      "description": "Defaults to the public production API.",
      "type": [
        "string",
        "null"
      ],
      "default": null
    }
  }
}

Output Schema

{
  "type": "object",
  "properties": {
    "items": {
      "description": "The trades on this page, in the requested sort order.",
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "number": {
            "description": "1-based order this trade closed in across the whole backtest (not just this page).",
            "type": "integer"
          },
          "entrySignalCandleOpenTime": {
            "description": "Candle open time this trade's entry signal fired on. Currently always equal to EntryExecutionCandleOpenTime — reserved for a future delayed-fill model (e.g. signal on candle close, execution on next candle open), not yet implemented. Don't rely on these differing today.",
            "type": "integer"
          },
          "entryExecutionCandleOpenTime": {
            "description": "Candle open time this trade's entry actually filled on. Currently always equal to EntrySignalCandleOpenTime — see that field's note.",
            "type": "integer"
          },
          "entryPrice": {
            "description": "Fill price at entry — always the entry candle's close.",
            "type": "number"
          },
          "exitSignalCandleOpenTime": {
            "description": "Candle open time this trade's exit signal fired on. Currently always equal to ExitExecutionCandleOpenTime — same reserved-for-future-use note as the entry pair.",
            "type": "integer"
          },
          "exitExecutionCandleOpenTime": {
            "description": "Candle open time this trade's exit actually filled on. Currently always equal to ExitSignalCandleOpenTime — see that field's note.",
            "type": "integer"
          },
          "exitPrice": {
            "description": "Fill price at exit — StopPrice, TakePrice, or the exit candle's close, depending on ExitReason.",
            "type": "number"
          },
          "pnlR": {
            "description": "This trade's PnL in R-units (risk multiples).",
            "type": "number"
          },
          "pnlPct": {
            "description": "This trade's PnL as a percent price move.",
            "type": "number"
          },
          "feeR": {
            "description": "R-units subtracted from this trade's PnlR by configuration.entryFeePct/exitFeePct (0 if neither was set). PnlR is already net of this — PnlR + FeeR recovers the pre-fee raw R. Not directly comparable across trades with different RiskDistance: this scales inversely with each trade's own stop distance.",
            "type": "number"
          },
          "stopPrice": {
            "description": "The stop-loss price this trade was risk-managed against.",
            "type": "number"
          },
          "takePrice": {
            "description": "The take-profit price this trade was risk-managed against.",
            "type": "number"
          },
          "riskDistance": {
            "description": "Price distance between EntryPrice and StopPrice — the denominator PnlR/FeeR are expressed in units of.",
            "type": "number"
          },
          "holdingCandles": {
            "description": "Timeframe-agnostic candle count the position was held for (exit candle index minus entry candle index). Minimum possible value is 1, not 0 — a position opened on candle i can earliest close on candle i+1. Derive real elapsed time from EntryExecutionCandleOpenTime/ExitExecutionCandleOpenTime if needed.",
            "type": "integer"
          },
          "exitReason": {
            "description": "\"StopLoss\", \"TakeProfit\", or \"Signal\" (closed via decision.exit).",
            "type": "string"
          },
          "conditionsAtEntry": {
            "description": "Every named condition from strategySnapshotJson.conditions at entry time — true AND false, not just the ones that were true.",
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": {
              "type": "boolean"
            }
          },
          "scoreBreakdownAtEntry": {
            "description": "Weight contributed by each condition toward TotalScoreAtEntry — 0 for conditions that were false.",
            "type": [
              "object",
              "null"
            ],
            "additionalProperties": {
              "type": "integer"
            }
          },
          "totalScoreAtEntry": {
            "description": "Total score at entry — the value compared against the threshold in decision.entry.",
            "type": [
              "integer",
              "null"
            ]
          }
        }
      }
    },
    "totalCount": {
      "description": "Total closed trades across the whole backtest, not just this page.",
      "type": "integer"
    },
    "page": {
      "type": "integer"
    },
    "pageSize": {
      "type": "integer"
    },
    "totalPages": {
      "type": "integer"
    }
  }
}
🟢get_backtest_result(id, backtestApiKey, waitForCompletion, pollTimeoutMs, backtestBaseUrl)

Fetches a submitted backtest by id. By default (waitForCompletion: true) polls internally until it finishes, so one call returns one final answer — no need to poll from the caller's side. Only aggregate metrics are returned here, no trade-by-trade detail — call get_backtest_trades for that (paginated, sortable). Fetching many results from the same submit_backtest_batch call? Use get_backtest_batch_results instead.

Input Schema

{
  "type": "object",
  "properties": {
    "id": {
      "description": "The id returned by submit_backtest.",
      "type": [
        "string",
        "null"
      ],
      "default": null
    },
    "backtestApiKey": {
      "description": "Your EmidLabs backtest API key. Not needed if this connector was added with a static 'x-api-key' header.",
      "type": [
        "string",
        "null"
      ],
      "default": null
    },
    "waitForCompletion": {
      "description": "If true (default), polls internally until the backtest finishes or pollTimeoutMs elapses.",
      "type": "boolean",
      "default": true
    },
    "pollTimeoutMs": {
      "description": "Defaults to 120000 (2 minutes).",
      "type": "integer",
      "default": 120000
    },
    "backtestBaseUrl": {
      "description": "Defaults to the public production API.",
      "type": [
        "string",
        "null"
      ],
      "default": null
    }
  }
}

Output Schema

{
  "type": "object",
  "properties": {
    "id": {
      "type": "string"
    },
    "status": {
      "type": "string"
    },
    "errorMessage": {
      "type": [
        "string",
        "null"
      ]
    },
    "result": {
      "type": [
        "object",
        "null"
      ],
      "properties": {
        "direction": {
          "description": "\"long\" or \"short\" — the direction the whole backtest traded.",
          "type": "string"
        },
        "pnlR": {
          "description": "Net profit/loss in R-units (risk multiples) — sum of every trade's PnL. This is the headline result.",
          "type": "number"
        },
        "trades": {
          "description": "Total number of closed trades.",
          "type": "integer"
        },
        "winRate": {
          "description": "wins / trades. Range 0–1.",
          "type": "number"
        },
        "wins": {
          "type": "integer"
        },
        "losses": {
          "type": "integer"
        },
        "grossProfitR": {
          "description": "Sum of all winning trades, in R-units.",
          "type": "number"
        },
        "grossLossR": {
          "description": "Sum of all losing trades, in R-units (negative).",
          "type": "number"
        },
        "profitFactor": {
          "description": "grossProfitR / abs(grossLossR). Greater than 1 means profitable. Null when there are no losing trades — the ratio is undefined (division by zero), not infinite.",
          "type": [
            "number",
            "null"
          ]
        },
        "avgWinR": {
          "description": "Average R on winning trades.",
          "type": "number"
        },
        "avgLossR": {
          "description": "Average R on losing trades (-1.0 with the default riskManagement; varies if the strategy overrides riskManagement.stopLoss).",
          "type": "number"
        },
        "expectancyR": {
          "description": "Average expected R per trade — (winRate * avgWinR) + (lossRate * avgLossR). This is the metric to optimize a strategy on, not raw winRate or trade count: a low win rate with a high avgWinR/avgLossR ratio can still have a strongly positive expectancyR.",
          "type": "number"
        },
        "totalFeeR": {
          "description": "Total R subtracted across all trades by configuration.entryFeePct/exitFeePct (0 if neither was set on the request). expectancyR/pnlR above are already net of this — TotalFeeR is just how much fees cost, for diagnostics. Caveat: a trade's fee-in-R cost scales inversely with that trade's own stop distance, so once fees are applied, expectancyR is only a fair comparison WITHIN one archetype's own stop convention — use pnlPct-based metrics (see get_backtest_trades) for comparisons across strategies/timeframes with different typical stop widths.",
          "type": "number"
        },
        "bothHit": {
          "description": "Trades where both stop-loss and take-profit were hit on the same candle (resolved as stop-loss). A high BothHit relative to Trades means many trades' outcome was decided by the engine's stop-wins-ties precedence rule rather than real intracandle price path data — treat results with more skepticism the higher this ratio is.",
          "type": "integer"
        },
        "maxDrawdownR": {
          "description": "Worst peak-to-trough dip across closed trades, in R-units. 0 if equity never fell below its running high-water mark. Historical/all-time — see CurrentDrawdownR for where the equity curve sits right now.",
          "type": "number"
        },
        "currentDrawdownR": {
          "description": "How far below its own peak the equity curve sits at the end of the backtest window, in R-units. 0 if the backtest ends at a new high. Includes any still-open position's unrealized PnL (see UnrealizedPnlRAtEnd) — a slump caused by an open, underwater position at window end shows up here.",
          "type": "number"
        },
        "openPositionsAtEnd": {
          "description": "Number of positions still open (never hit stop/take/exit-signal) when the backtest's date range ended. 0 in the common case. Check this before trusting CurrentDrawdownR/UnrealizedPnlRAtEnd at face value.",
          "type": "integer"
        },
        "unrealizedPnlRAtEnd": {
          "description": "Sum of unrealized PnL, in R-units, across all positions still open at window end — marked to market against the last available candle's close. 0 when OpenPositionsAtEnd is 0. Does NOT include exit fee (the position hasn't closed, so none has been paid) — a slight overestimate of true current drawdown when a position is open. This value feeds ONLY CurrentDrawdownR/MaxDrawdownR — it is never included in PnlR/ExpectancyR/Trades/Wins/Losses or any other metric describing closed, realized trades.",
          "type": "number"
        },
        "conditionsDistributionPct": {
          "description": "For each possible count of simultaneously-true conditions (0, 1, 2...N): fraction of all candles where exactly that many were true at once. A strategy-tuning diagnostic (how selective is the entry setup), not a performance metric.",
          "type": "object",
          "additionalProperties": {
            "type": "number"
          }
        },
        "scoreDistributionPct": {
          "description": "For each possible total score value: fraction of candles that summed to it.",
          "type": "object",
          "additionalProperties": {
            "type": "number"
          }
        },
        "confirmedSignalsCount": {
          "description": "Only meaningful when the request declared confirmationSources — how many candidates cleared confirmation and became one of the Trades above.",
          "type": "integer"
        },
        "unconfirmedSignalsCount": {
          "description": "Only meaningful when the request declared confirmationSources — how many candidates were dropped before trade simulation because they weren't corroborated. ConfirmedSignalsCount + UnconfirmedSignalsCount is the total candidate count, same as what a backtest without confirmationSources would have produced.",
          "type": "integer"
        }
      }
    },
    "runtimeMs": {
      "description": "How long the analyser actually took to run this backtest, in milliseconds — pure compute time, not counting queue/messaging latency. Null until Status is \"Completed\".",
      "type": [
        "integer",
        "null"
      ]
    },
    "candlesProcessed": {
      "description": "Number of candles the analyser processed for this backtest. Null until Status is \"Completed\". This is raw volume, not the plan's billing unit — see UnitsConsumed for that.",
      "type": [
        "integer",
        "null"
      ]
    },
    "unitsConsumed": {
      "description": "The actual execution-unit cost of this backtest, matching what's debited from the account's plan balance (CandlesProcessed normalized by the account's candles-per-unit rate). Null until Status is \"Completed\".",
      "type": [
        "number",
        "null"
      ]
    },
    "recentTradeCount": {
      "description": "Number of trades the recency fields below are based on (up to 5, fewer if the backtest has fewer trades). Null until Status is \"Completed\".",
      "type": [
        "integer",
        "null"
      ]
    },
    "recentAvgPnlR": {
      "description": "Average pnlR of the most recent RecentTradeCount closed trades. This is the recency signal for a rolling ranking — weighted alongside expectancyR, not a replacement for it.",
      "type": [
        "number",
        "null"
      ]
    },
    "recentOutcomes": {
      "description": "\"Win\"/\"Loss\" per recent trade, chronological — oldest first, so the LAST element is the most recent trade. Lets a caller see whether recent trades were genuinely a streak (e.g. all \"Loss\") versus alternating, which RecentAvgPnlR alone can't distinguish.",
      "type": [
        "array",
        "null"
      ],
      "items": {
        "type": "string"
      }
    }
  }
}
🟡submit_backtest(assetPair, initialDate, finalDate, strategySnapshotJson, confirmationSources, ...)

Submits a strategy for backtesting against historical OHLCV data. Returns immediately with an id and status — call get_backtest_result to fetch the outcome once it finishes. Testing the same strategy against many assets? Use submit_backtest_batch instead — one call per asset here adds up fast. Optionally accepts confirmationSources to only count a candidate Entry/Exit as a real trade once corroborated by a submit_confirmation_source result (multi-timeframe or cross-asset confirmation) — see the confirmationSources argument and submit_confirmation_source's own description.

Input Schema

{
  "type": "object",
  "properties": {
    "assetPair": {
      "description": "e.g. \"BTC-USDC\".",
      "type": [
        "string",
        "null"
      ],
      "default": null
    },
    "initialDate": {
      "description": "ISO date string, e.g. \"2025-01-01\".",
      "type": [
        "string",
        "null"
      ],
      "default": null
    },
    "finalDate": {
      "description": "ISO date string, e.g. \"2025-06-01\".",
      "type": [
        "string",
        "null"
      ],
      "default": null
    },
    "strategySnapshotJson": {
      "description": "The Strategy DSL object — every field below documents its own exact shape, this is just the execution model that ties them together. Entry fills at the close of the candle where decision.entry turns true (no lookahead). A position closes on the first of these to happen, checked in this order: stop-loss hit, take-profit hit, decision.exit turning true (a same-candle stop/take-profit always wins over exit). Multiple positions can be open at once by default — cap with configuration.maxOpenPositions. Results are measured in R-units (risk multiples); expectancyR (average R per trade) is the metric to optimize, not raw win rate or trade count.",
      "type": [
        "object",
        "null"
      ],
      "properties": {
        "configuration": {
          "type": [
            "object",
            "null"
          ],
          "properties": {
            "timeframe": {
              "description": "One of \"5M\", \"15M\", \"30M\", \"1H\", \"2H\", \"4H\", \"1D\".",
              "type": [
                "string",
                "null"
              ]
            },
            "warmupBars": {
              "description": "Leading candles excluded from trading while indicators stabilize. Default 0.",
              "type": "integer"
            },
            "maxOpenPositions": {
              "description": "Caps concurrent open trades. Omit/null for unlimited (default).",
              "type": [
                "integer",
                "null"
              ]
            },
            "direction": {
              "description": "\"long\" or \"short\" — picks which side the WHOLE backtest trades, not a mix. Only affects stop-loss/take-profit price placement and the profit/loss sign (mirrored for short: stop above entry, take below entry, profit when price falls) — conditions/score/decision keep the exact same meaning either way. Defaults to \"long\".",
              "type": [
                "string",
                "null"
              ]
            },
            "entryFeePct": {
              "description": "Simulated exchange fee on entry, as a percent (e.g. 0.1 = 0.1%). Omit/null = 0 (no fee, backward compatible). Subtracted from every trade's pnlR/pnlPct so results are net-of-fee by construction. 0-5 range. See the pnlR/expectancyR field descriptions on get_backtest_result for the R-vs-% caveat once a fee is set.",
              "type": [
                "number",
                "null"
              ]
            },
            "exitFeePct": {
              "description": "Simulated exchange fee on exit, as a percent (e.g. 0.1 = 0.1%). Same semantics as EntryFeePct — real exchanges can charge different maker/taker rates per leg, so this is independent, not assumed equal.",
              "type": [
                "number",
                "null"
              ]
            },
            "timezone": {
              "description": "IANA timezone id (e.g. \"America/Sao_Paulo\") used to localize the DSL's hour()/minute()/dayOfWeek()/isWeekend() functions, for time-of-day or weekday gating conditions. Omit/null defaults to \"America/Sao_Paulo\".",
              "type": [
                "string",
                "null"
              ]
            }
          }
        },
        "inputs": {
          "description": "Named computed values, one JSON-string expression each, evaluated once per candle. Raw fields: close, open, high, low, volume. Built-in functions (case-sensitive, lowercase — no others exist, e.g. avg()/SMA() uppercase/close[N] bracket indexing are NOT supported): ema(series,period), sma(series,period), rsi(series,period), atr(period), adx(period), adxPlusDi(period), adxMinusDi(period), crossUp(a,b), crossDown(a,b), highest(series,n), lowest(series,n), change(series), shift(series,n) [look-back only, NaN before enough history], any(boolSeries,n), all(boolSeries,n), count(boolSeries,n) [over the n candles before the current one], swingHigh(series,confirmBars), swingLow(series,confirmBars) [confirmed N-bar swing point, true confirmBars candles AFTER the actual peak/trough — never at the peak itself, so it can't repaint between backtest and live], body(), range(), upperWick(), lowerWick(), isBullish(), isBearish(), abs(x), min(a,b), max(a,b). There is no volumeSma()/volumeSpike() — volume is a plain series like close/open/high/low, so use sma(volume,period) and volume > sma(volume,period) * multiplier instead. Candlestick patterns (all no-arg, boolean, read only closed-candle OHLC, same in backtest and live): single-candle hammer(), shootingStar(), doji(), bullishMarubozu(), bearishMarubozu(), spinningTop(), dragonflyDoji(), gravestoneDoji(), longLeggedDoji(); two-candle bullishEngulfing(), bearishEngulfing(), piercingLine(), darkCloudCover(), bullishHarami(), bearishHarami(), haramiCross(), tweezerTop(), tweezerBottom(); three-plus-candle morningStar(), eveningStar(), threeWhiteSoldiers(), threeBlackCrows(), threeInsideUp(), threeInsideDown(), threeOutsideUp(), threeOutsideDown(), risingThreeMethods(), fallingThreeMethods(). Caveat: hammer()/shootingStar() are shape-only, no prior-trend check (same shape is Hammer in a downtrend but Hanging Man in an uptrend, and vice versa for shootingStar/Inverted Hammer) — pair with a trend/momentum condition rather than using the shape alone. An input may only reference inputs defined above it (no forward/circular references). Example: {\"emaFast\":\"ema(close, 9)\",\"emaSlow\":\"ema(close, 21)\"}",
          "type": [
            "object",
            "null"
          ],
          "additionalProperties": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "conditions": {
          "description": "Named boolean expressions over inputs/market data. Operators: > < >= <= == != AND OR. Example: {\"trendUp\":\"emaFast > emaSlow AND adx14 > 20\"}",
          "type": [
            "object",
            "null"
          ],
          "additionalProperties": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "score": {
          "description": "Integer weight per condition name (keys must match \"conditions\"). The engine sums the weights of every true condition on a candle; compare the total against a threshold in \"decision.entry\" (e.g. \"score >= 50\").",
          "type": [
            "object",
            "null"
          ],
          "additionalProperties": {
            "type": "integer"
          }
        },
        "decision": {
          "type": [
            "object",
            "null"
          ],
          "properties": {
            "entry": {
              "description": "Boolean expression evaluated per candle; a trade opens when true.",
              "type": [
                "string",
                "null"
              ]
            },
            "exit": {
              "description": "Optional boolean expression; closes an open position when true, independent of riskManagement. If a candle's stop-loss/take-profit and \"exit\" would both trigger, the stop-loss/take-profit wins and exit is not evaluated for that position that candle. If omitted, positions only close via stop-loss/take-profit.",
              "type": [
                "string",
                "null"
              ]
            }
          }
        },
        "riskManagement": {
          "description": "Configures stop-loss/take-profit. Omit entirely for the default (1% stop-loss, 1:3 risk-reward take-profit) — see stopLoss/takeProfit below for the full shape of each.",
          "type": [
            "object",
            "null"
          ],
          "properties": {
            "stopLoss": {
              "description": "Stop-loss policy. Exactly one of: {\"type\":\"percent\",\"percent\":<number, e.g. 1.0 for 1%>} or {\"type\":\"atr\",\"period\":<int>,\"multiplier\":<number>}. \"type\" is required; field order does not matter."
            },
            "takeProfit": {
              "description": "Take-profit policy. Exactly one of: {\"type\":\"riskReward\",\"multiple\":<number>} (distance = stop-loss distance * multiple) or {\"type\":\"percent\",\"percent\":<number>}. \"type\" is required; field order does not matter."
            }
          }
        }
      },
      "default": null
    },
    "confirmationSources": {
      "description": "Optional. Each entry names a submit_confirmation_source result (same account only, must already be Completed) that every candidate Entry/Exit must be corroborated by before it's simulated as a trade — an unconfirmed candidate is dropped before trade simulation, never appears in get_backtest_trades or affects PnlR/WinRate/etc. See ConfirmedSignalsCount/UnconfirmedSignalsCount on get_backtest_result. A sourceId that doesn't exist, isn't Completed, or belongs to another account fails this submission immediately (unlike live, this is synchronous/batch — letting it through would produce a confusing zero-trade result with no explanation).",
      "type": [
        "array",
        "null"
      ],
      "items": {
        "description": "One confirmation requirement: this backtest's own candidate Entry/Exit only counts as a trade once sourceId's own signal timeline is corroborated within validityWindow.",
        "type": [
          "object",
          "null"
        ],
        "properties": {
          "sourceId": {
            "description": "The id returned by submit_confirmation_source (same account only) whose signal timeline this backtest's candidates must be corroborated by.",
            "type": [
              "string",
              "null"
            ]
          },
          "signalType": {
            "description": "\"entry\" or \"exit\" — which of the source's signal types counts as confirming. Defaults to \"entry\" when omitted.",
            "type": [
              "string",
              "null"
            ]
          },
          "validityWindow": {
            "description": "An OBJECT, not a bare number — e.g. { \"count\": 5 }. Defaults to { \"count\": 1 } when omitted.",
            "type": [
              "object",
              "null"
            ],
            "properties": {
              "count": {
                "description": "Number of candles of the source's own timeframe. Defaults to 1 when omitted.",
                "type": "integer"
              }
            }
          }
        },
        "required": [
          "sourceId"
        ]
      },
      "default": null
    },
    "backtestApiKey": {
      "description": "Your EmidLabs backtest API key (created in the Console). Not needed if this connector was added with a static 'x-api-key' header.",
      "type": [
        "string",
        "null"
      ],
      "default": null
    },
    "backtestBaseUrl": {
      "description": "Defaults to the public production API. Override only for self-hosted/staging use.",
      "type": [
        "string",
        "null"
      ],
      "default": null
    }
  }
}

Output Schema

{
  "type": "object",
  "properties": {
    "id": {
      "type": "string"
    },
    "status": {
      "type": "string"
    }
  }
}
🟢get_confirmation_source(id, backtestApiKey, waitForCompletion, pollTimeoutMs, backtestBaseUrl)

Fetches a submitted confirmation source by id. By default (waitForCompletion: true) polls internally until it finishes, so one call returns one final answer. No trade-outcome fields here at all (no PnlR/WinRate/...) — a confirmation source never opens a position. Once Completed, its id can be used as a confirmationSources sourceId in submit_backtest; its raw signal timeline is available separately via get_confirmation_source_signals.

Input Schema

{
  "type": "object",
  "properties": {
    "id": {
      "description": "The id returned by submit_confirmation_source.",
      "type": [
        "string",
        "null"
      ],
      "default": null
    },
    "backtestApiKey": {
      "description": "Your EmidLabs backtest API key. Not needed if this connector was added with a static 'x-api-key' header.",
      "type": [
        "string",
        "null"
      ],
      "default": null
    },
    "waitForCompletion": {
      "description": "If true (default), polls internally until the confirmation source finishes or pollTimeoutMs elapses.",
      "type": "boolean",
      "default": true
    },
    "pollTimeoutMs": {
      "description": "Defaults to 120000 (2 minutes).",
      "type": "integer",
      "default": 120000
    },
    "backtestBaseUrl": {
      "description": "Defaults to the public production API.",
      "type": [
        "string",
        "null"
      ],
      "default": null
    }
  }
}

Output Schema

{
  "type": "object",
  "properties": {
    "id": {
      "type": "string"
    },
    "assetPair": {
      "type": "string"
    },
    "timeframe": {
      "type": "string"
    },
    "status": {
      "type": "string"
    },
    "errorMessage": {
      "type": [
        "string",
        "null"
      ]
    },
    "runtimeMs": {
      "description": "How long the analyser actually took to compute this confirmation source's signal timeline, in milliseconds. Null until Status is \"Completed\".",
      "type": [
        "integer",
        "null"
      ]
    },
    "candlesProcessed": {
      "description": "Number of candles the analyser processed. Null until Status is \"Completed\". This is raw volume, not the plan's billing unit — see UnitsConsumed for that.",
      "type": [
        "integer",
        "null"
      ]
    },
    "unitsConsumed": {
      "description": "The actual execution-unit cost, same pool/rate as a regular backtest — computing a confirmation source's signal timeline costs the same per candle as simulating trades, it just skips the trade simulation itself.",
      "type": [
        "number",
        "null"
      ]
    }
  }
}
🟢get_confirmation_source_signals(id, page, pageSize, backtestApiKey, backtestBaseUrl)

Fetches a completed confirmation source's raw signal timeline, paginated — the actual data a backtest's confirmationSources requirement is checked against. Can easily run into the thousands for a frequent condition over a long range (every candle-condition match is a row, not just closed trades), which is why this is paginated from the start. Debugging/inspection only — you do NOT need this tool to make confirmationSources work, and should not fetch these rows to reconstruct the gating yourself. To actually gate a backtest by this source, pass its id directly as sourceId in submit_backtest's own confirmationSources array; the backend applies the backward-only state check against this exact timeline automatically, including the higher-timeframe-to-lower-timeframe alignment. Reimplementing that alignment by hand from this raw data is unnecessary and easy to get wrong (e.g. failing to correctly persist a higher-timeframe state across every lower-timeframe candle until the next higher-timeframe close). Use this tool only to sanity-check a source's signal density or diagnose an unexpectedly low ConfirmedSignalsCount after the fact.

Input Schema

{
  "type": "object",
  "properties": {
    "id": {
      "description": "The id returned by submit_confirmation_source.",
      "type": [
        "string",
        "null"
      ],
      "default": null
    },
    "page": {
      "description": "1-based page number. Default 1.",
      "type": "integer",
      "default": 1
    },
    "pageSize": {
      "description": "Signals per page, 1-100. Default 20.",
      "type": "integer",
      "default": 20
    },
    "backtestApiKey": {
      "description": "Your EmidLabs backtest API key. Not needed if this connector was added with a static 'x-api-key' header.",
      "type": [
        "string",
        "null"
      ],
      "default": null
    },
    "backtestBaseUrl": {
      "description": "Defaults to the public production API.",
      "type": [
        "string",
        "null"
      ],
      "default": null
    }
  }
}

Output Schema

{
  "type": "object",
  "properties": {
    "items": {
      "description": "The signals on this page, in candle order.",
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "candleOpenTime": {
            "description": "Open time (unix epoch) of the candle whose close triggered this signal.",
            "type": "integer"
          },
          "type": {
            "description": "\"Entry\" or \"Exit\".",
            "type": "string"
          },
          "score": {
            "description": "Total score at signal time.",
            "type": "integer"
          },
          "conditions": {
            "description": "Every named condition from strategySnapshotJson.conditions at signal time — true AND false, not just the ones that were true.",
            "type": "object",
            "additionalProperties": {
              "type": "boolean"
            }
          },
          "scoreBreakdown": {
            "description": "Weight contributed by each condition toward Score.",
            "type": "object",
            "additionalProperties": {
              "type": "integer"
            }
          }
        }
      }
    },
    "totalCount": {
      "description": "Total signals across the whole confirmation source, not just this page — can easily be in the thousands for a frequent condition over a long range.",
      "type": "integer"
    },
    "page": {
      "type": "integer"
    },
    "pageSize": {
      "type": "integer"
    },
    "totalPages": {
      "type": "integer"
    }
  }
}

Community

Rate this Server

Evidence

Recent observations

verifiedversion not recorded9 tools
verifiedversion not recorded9 tools