Oracle backtest & data query

This page covers the single-strategy subset of the Oracle engine plus the corpus query API. For multi-strategy parameter sweeps see Experiments; for the curated paper-trading dashboard surface see Deployed strategies.
Disambiguation. /api/v1/oracle/backtest* (this page) runs through Mangrove Oracle — a faster engine optimised for batch and sweep workloads. /api/v1/backtesting/run (see Backtesting) runs through the legacy MangroveAI strategy engine. New code should target the Oracle endpoints documented here.
All endpoints sit under /api/v1/oracle/ on the MangroveAI gateway and are forwarded to the Oracle engine with your identity attached. Auth: API key or JWT, same as the rest of /api/v1/*.
SDK ↔ server contract note (data query only). The DataQueryResponse Pydantic model in mangroveai Python SDK 1.4.0 defines a table: str field that the actual server response does NOT include — the typed wrapper fails validation on every successful 200. Separately, DataQueryRequest.order_by is typed as a list of strings whereas the server expects [{col, dir}] dict shape. The /data/query Python example below uses raw HTTP for this reason. Tracking fixes: SDK#16, SDK#17.The historical OracleBacktestRequest / OracleBulkBacktestRequest gap (risk-mgmt fields typed Optional but server-rejected) was closed by MangroveAI v3.10.12 — the gateway proxy now fills missing risk-mgmt fields from canonical trading_defaults.json, so the Optional typing is honored end-to-end and the SDK calls below work with just asset + strategy_json + a date range.

Endpoints

MethodPathMeterx402 priceQuota counter
POST/api/v1/oracle/backtestoracle_backtest$0.10api_calls
POST/api/v1/oracle/backtest/asyncoracle_backtest$0.10api_calls
GET/api/v1/oracle/backtest/async/{backtest_id}oracle_backtest$0.10api_calls
POST/api/v1/oracle/backtest/bulkoracle_backtest$0.10api_calls
POST/api/v1/oracle/data/queryoracle_data_query$0.10oracle_data_queries
Every HTTP call counts as 1 billable unit, regardless of how many strategies the bulk endpoint fans out to or how many rows the query returns. Async poll requests are billed at the same rate as the submit — design your polling loop accordingly (5-10s intervals are conventional). The bulk endpoint amortises efficiently: a single billed call can drive up to 99 strategies through shared OHLCV fetches. Pricing source-of-truth: MangroveAI/src/MangroveAI/domains/backtesting/oracle_proxy.py::_PROXY_METERING.

Synchronous single-strategy backtest

POST /api/v1/oracle/backtest Runs one strategy against one (asset, interval) pair and blocks until the engine returns the full result. Typical wall-clock is 30–120 seconds depending on the date range; the SDK default HTTP timeout is 180s.

Required fields

18 top-level fields, enforced by MangroveOracle/src/api/routes/backtest.py::run_backtest:
  • asset, strategy_json — what to run
  • initial_balance, min_balance_threshold, min_trade_amount — account
  • max_open_positions, max_trades_per_day, max_risk_per_trade, max_units_per_trade, max_trade_amount — risk caps
  • volatility_window, target_volatility, volatility_mode, enable_volatility_adjustment — volatility-aware sizing
  • cooldown_bars, daily_momentum_limit, weekly_momentum_limit — circuit breakers
Plus a top-level execution_config containing at least position_size_calc: "v2" (v1 legacy also accepted). The engine raises position_size_calc is required if it’s missing. Optional: interval (defaults to "1h"), lookback_months, start_date, end_date (provide one of: explicit start+end, start alone (defaults end to now), or lookback_months). Defaults filled by the gateway. Since MangroveAI v3.10.12 the proxy merges canonical values from trading_defaults.json into any /backtest* request body before forwarding to Oracle — customer-supplied values always win (customer-wins-only setdefault). So in practice you can call /backtest with just asset, strategy_json, and a date range; the 15 risk-mgmt fields and a full execution_config will be populated for you. Pull the same canonical values explicitly via GET /api/v1/oracle/exec-config/defaults when you want to inspect or override them.

Request

{
  "asset": "BTC",
  "interval": "1h",
  "strategy_json": "{ \"asset\": \"BTC\", \"interval\": \"1h\", \"entry\": [...], \"exit\": [...] }",
  "lookback_months": 3,

  "execution_config": {
    "position_size_calc": "v2",
    "reward_factor": 2,
    "atr_period": 14,
    "atr_volatility_factor": 2.0,
    "atr_short_weight": 0.95,
    "atr_long_weight": 0.05,
    "atr_cap_multiplier": 2.1
  },

  "initial_balance": 10000,
  "min_balance_threshold": 100,
  "min_trade_amount": 25,
  "max_open_positions": 1,
  "max_trades_per_day": 5,
  "max_risk_per_trade": 0.02,
  "max_units_per_trade": 1.0,
  "max_trade_amount": 5000,
  "volatility_window": 14,
  "target_volatility": 0.20,
  "volatility_mode": "off",
  "enable_volatility_adjustment": false,
  "cooldown_bars": 24,
  "daily_momentum_limit": 3,
  "weekly_momentum_limit": 3
}

Response

{
  "success": true,
  "metrics": {
    "sharpe": 1.42, "sortino": 1.88, "calmar": 0.94,
    "max_drawdown": 0.31, "win_rate": 0.58, "total_trades": 47,
    "final_balance": 11820.5, "...": "..."
  },
  "execution_time_seconds": 42.1,
  "trade_count": 47,
  "strategy_names": ["..."],
  "trade_history": [{"...": "..."}],
  "denied_signals": []
}
denied_signals lists signals the engine refused (unsupported columns, malformed params). Check this when trade_count is lower than expected.

Example

curl -X POST https://api.mangrovedeveloper.ai/api/v1/oracle/backtest \
  -H "Authorization: Bearer $MANGROVE_API_KEY" \
  -H "Content-Type: application/json" \
  -d @backtest_request.json

Async submit + poll

When 30-120s of blocking is too long — dispatching dozens of backtests from a worker that should stay responsive — use the async pair.

Submit

POST /api/v1/oracle/backtest/async Same request body as the sync endpoint (same 18 required fields + top-level execution_config). Returns immediately:
{ "backtest_id": "bkt_20260603T140212548Z", "status": "queued" }

Poll

GET /api/v1/oracle/backtest/async/{backtest_id}
{
  "backtest_id": "bkt_20260603T140212548Z",
  "status": "running",
  "metrics": null,
  "trade_history": null
}
status transitions queuedrunningcompleted (or failed). Once completed, the response carries the full metrics/trade_history/trade_count shape of the synchronous response. Polling is metered at the same rate as submit ($0.10 / api_calls unit per call); space your polls accordingly.

Example

Python (SDK)
import json, time
from mangrove_ai import MangroveAI
from mangrove_ai.models.oracle import OracleBacktestRequest

client = MangroveAI(api_key=API_KEY)

# Submit (same request shape as sync /backtest — gateway fills defaults)
submit = client.oracle.backtest_async(OracleBacktestRequest(
    asset="BTC",
    interval="1h",
    strategy_json=json.dumps(strategy),
    lookback_months=3,
))
backtest_id = submit.backtest_id

# Poll
while True:
    status = client.oracle.backtest_poll(backtest_id)
    if status.status in ("completed", "failed"):
        break
    time.sleep(10)

print(f"final status={status.status} trade_count={status.trade_count}")

Bulk

POST /api/v1/oracle/backtest/bulk Evaluates many strategies over a shared date range with shared OHLCV fetches. One HTTP call, one bill, up to 99 strategies. The right endpoint when you have a SIEVE-filtered shortlist and want to score the survivors quickly — see End-to-end workflow with SIEVE for the full workflow.

Required fields

17 top-level fields + EXACTLY ONE of strategy_ids OR strategy_configs (not both), per MangroveOracle/src/api/routes/backtest.py::run_bulk_backtest:
  • start_date, end_date — required (no lookback_months shortcut on bulk)
  • The same 15 risk-mgmt fields as sync (sans strategy_json since each item carries its own)
  • Top-level execution_config with position_size_calc
strategy_configs is [{asset, interval, strategy_json}, …]; strategy_ids is [uuid, …] referencing strategies already created via client.strategies.create(...) in MangroveAI.

Request

{
  "start_date": "2024-01-01T00:00:00Z",
  "end_date":   "2024-12-31T00:00:00Z",
  "strategy_configs": [
    { "asset": "BTC", "interval": "1h", "strategy_json": "{...}" },
    { "asset": "BTC", "interval": "1h", "strategy_json": "{...}" }
  ],
  "execution_config": { "position_size_calc": "v2" },

  "initial_balance": 10000,
  "min_balance_threshold": 100, "min_trade_amount": 25,
  "max_open_positions": 1, "max_trades_per_day": 5,
  "max_risk_per_trade": 0.02, "max_units_per_trade": 1.0, "max_trade_amount": 5000,
  "volatility_window": 14, "target_volatility": 0.20,
  "volatility_mode": "off", "enable_volatility_adjustment": false,
  "cooldown_bars": 24, "daily_momentum_limit": 3, "weekly_momentum_limit": 3
}

Response

{
  "success": true,
  "results": [
    {"success": true,  "strategy_name": "...", "strategy_id": "...",
     "metrics": {...}, "trade_count": 47, "execution_time_seconds": 12.4},
    {"success": false, "strategy_name": "...", "strategy_id": "...",
     "error": "...", "trade_count": 0, "metrics": {}}
  ],
  "data_fetches": 1,
  "total_execution_time_seconds": 51.2
}
Individual strategies fail soft — results[i].success == false carries an error field and an empty metrics, but the rest of the batch continues. Input order is preserved: results[i] corresponds to strategy_configs[i]. data_fetches reports how many unique OHLCV fetches the engine did across the strategy list (strategies sharing (asset, interval) reuse one fetch).

Example

Python (SDK)
import json
from mangrove_ai import MangroveAI
from mangrove_ai.models.oracle import OracleBulkBacktestRequest

client = MangroveAI(api_key=API_KEY)

# Risk-mgmt fields + execution_config filled by the gateway from
# trading_defaults.json when omitted — bulk shares the same merge path.
batch = client.oracle.backtest_bulk(OracleBulkBacktestRequest(
    start_date="2024-01-01T00:00:00Z",
    end_date="2024-12-31T00:00:00Z",
    strategy_configs=[
        {"asset": "BTC", "interval": "1h", "strategy_json": json.dumps(strategy_a)},
        {"asset": "BTC", "interval": "1h", "strategy_json": json.dumps(strategy_b)},
    ],
))

print(f"data_fetches={batch.data_fetches} total_time={batch.total_execution_time_seconds:.1f}s")
for r in batch.results:
    if r["success"]:
        print(f"  {r['strategy_name']}: trades={r['trade_count']} sharpe={r['metrics'].get('sharpe')}")
    else:
        print(f"  {r['strategy_name']}: FAILED — {r['error']}")

Data query (corpus)

POST /api/v1/oracle/data/query A constrained DSL over Oracle’s BigQuery-backed corpus of historical backtest results and OHLCV bars. The proxy enforces server-side: table whitelist, column whitelist, filter-op whitelist, tenancy injection (WHERE org_id = <caller>), and a per-query maximum_bytes_billed cap. Customers cannot send raw SQL.

Request

FieldTypeNotes
table"results" | "ohlcv"required
selectlist[str]required, 1-80 columns, whitelisted per table
filterslist[{col, op, value}]optional, ≤20 filters
order_bylist[{col, dir}]optional, ≤5 orderings; dir is "asc" or "desc"
limitintoptional, 1-1000, default 100
page_tokenstroptional; opaque continuation token from a prior response
filters[].op accepts: "=", "!=", ">", ">=", "<", "<=", "in", "between" (per MangroveOracle/src/models/data_query.py::FilterOp).

Response

{
  "rows": [{"...": "..."}],
  "row_count": 23,
  "total_bytes_billed": 10485760,
  "cost_estimate_usd": 0.0000596,
  "next_page_token": null
}
total_bytes_billed is the actual BigQuery-billed scan in bytes (used to compute the customer’s invoice line for this call); cost_estimate_usd is that figure converted at GCP’s published per-TB rate. next_page_token is non-null when more rows are available — pass it back as page_token to continue.

Example

curl -X POST https://api.mangrovedeveloper.ai/api/v1/oracle/data/query \
  -H "Authorization: Bearer $MANGROVE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "table": "results",
    "select": ["strategy_name", "asset", "timeframe"],
    "filters": [{"col": "asset", "op": "=", "value": "BTC"}],
    "order_by": [{"col": "strategy_name", "dir": "desc"}],
    "limit": 20
  }'

Costs (summary)

EndpointPer-callQuota counter
All four /backtest* endpoints$0.10 x402 / 1 unitapi_calls
/data/query$0.10 x402 / 1 unitoracle_data_queries
See Pricing for tier monthly caps. Source of truth: MangroveAI/src/MangroveAI/domains/backtesting/oracle_proxy.py::_PROXY_METERING.
  • Experiments — parameter-space sweeps where the engine generates its own candidates (createvalidatelaunch → poll). Different from bulk: bulk backtests a list you supply; a sweep searches a space. Sweep size is tier-bounded, not capped at the bulk 99.
  • SIEVE classifier — score 1-99 candidate strategies in milliseconds before paying for backtests.
  • Deployed strategies — live execution state for curated paper-trading strategies, not for ad-hoc backtests.
  • End-to-end workflow with SIEVE — end-to-end demo: list-signals, SIEVE screen, bulk-backtest the survivors, adopt.
  • mangroveai Python SDK — typed wrappers for all /backtest* endpoints land in SDK v1.4.0; the historical risk-mgmt gap is now closed at the gateway (MangroveAI v3.10.12). The /data/query Python example below still uses raw HTTP pending SDK#16 / SDK#17.