Documentation · v1.0.0

Market Oracle API

Market data for trading bots and AI agents: closed bars, indicators, order book, macro, and market regime. Below — everything in order: from your first request to ready-made code in Python and Node.js.

Full contract for an AI agent.
https://market-oracle.pro/en/docs/agents.md

Basics

Market Oracle is a market data source only. It does not open trades or manage positions. Between the market and your strategy sits a single prepared layer: closed bars, indicators, order-book facts, macro context, and market regime.

Core rule: trading decisions — only on a closed bar (closed: true). Each signal is processed once, deduplicated by the symbol:ts key.

Quick start

Five minutes to your first meaningful API response:

1. Health

Service is up, data is fresh.

GET /health

2. Plan

Your limits: requests per day, RPM, WS count.

GET /v1/me

3. Symbols

Take only pairs with ready=true.

GET /v1/symbols

4. Stream

Subscribe to closed bars.

WS /v1/stream

5. Context

Decision snapshot: every 60–90 s and before entry.

GET /v1/context/{symbol}

6. Signal

One analysis per closed bar.

dedupe symbol:ts
# Current node. Use the key from the registration email curl.exe -s https://api.market-oracle.pro/health curl.exe -s https://api.market-oracle.pro/v1/me -H "Authorization: Bearer mo_…" curl.exe -s "https://api.market-oracle.pro/v1/context/BTCUSDT" -H "Authorization: Bearer mo_…"

Connection

Two channels, two roles. REST pulls data (snapshots, history, calendar). WebSocket tells you when another bar closes. The trading loop is always the same: WS wakes you up → REST fetches the data → decide → wait. Oracle does not send webhooks (POST to your endpoint).

Address and key

# Keep in environment variables, not in code or public frontend ORACLE_BASE_URL=https://api.market-oracle.pro ORACLE_API_KEY=mo_…

Every request to /v1/* carries the header:

Authorization: Bearer mo_…

What comes from where

NeedChannelSource
The "bar closed" momentWebSocketbar_close (bar + 1m indicators)
Everything for a decision in one responseRESTGET /v1/context/{symbol}
Trade cost and "too expensive / fine"RESTtrade_cost / cost_risk fields in context (not in WS)
Higher timeframe, liveRESThtf field in context (m15/h1/h4/d1; not in WS)
Candle and indicator historyRESTGET /v1/history / POST /v1/history/batch
Fresh book to validate an entryRESTGET /v1/depth/{symbol}?seconds=3
Minute order-book historyREST or WSGET /v1/microstructure or the microstructure_close event
Event calendar, macro, market regimeREST/v1/calendar, /v1/macro, /v1/market-regime

WebSocket dialogue

# 1. After connect the server sends hello. No events before subscribe. {"type": "hello", "version": "1.0.0", …} # 2. Subscribe: selected pairs, all at once, or with the book {"op": "subscribe", "symbols": ["BTCUSDT", "ETHUSDT"]} {"op": "subscribe", "symbols": ["*"]} {"op": "subscribe", "symbols": ["BTCUSDT"], "microstructure": true} # 3. Signal and book events (join strictly by symbol + ts) {"type": "bar_close", "symbol": "BTCUSDT", "interval": "1m", "bar": {…}} {"type": "microstructure_close", "symbol": "BTCUSDT", "microstructure": {…}} # 4. Housekeeping {"op": "unsubscribe", "symbols": ["ETHUSDT"]} {"op": "ping"} → pong
Reconnect must use backoff: start 1 s → ×2 → max 60 s, jitter ±25%. Don't set the delay in milliseconds — on mass client restarts it hammers the server. After reconnect, first backfill the gaps via /v1/history, then resume live.

Symbol readiness

Not every listed pair is fit for analysis. Checking this flag bundle takes three seconds and saves you from trading on "warm" history:

FieldWhereMeaning
readysymbols, status, contexttrue at ≥ 200 bars (EMA200 warmup)
historysamewarming / ready / full_day (≥1440 bars)
indicators_readylatest, status, contextbase set ema20+rsi14+atr14 present
data_quality.scorecontext, status0…100; for entry require ≥ 70
lag_secstatus, contextage of the closed bar; >120 cuts score to ≤69
last_closed_tssymbols, statuslast closed bar time (ms UTC)
Important: readyindicators_ready. EMA200 strategies need both. Don't trade a pair with 15 candles as if it were BTC with a full day of history.

GET /v1/context/{symbol}

The main endpoint for bots and agents: one response instead of 8–10 calls. Pull it every 60–90 s per active pair and always before entry.

BlockWhat it is, in plain words
bar + indicatorslast closed 1m bar — the only signal source
tickercurrent price and 24h stats (for display and sizing, not for signals)
depthfresh book: spread, bid/ask skew, large levels
trade_cost / cost_riskwhat the trade costs and whether fees eat the stop (tradable: false = skip). Available only here, not in WS
htfhigher bars, live: m15/h1/h4/d1 with indicators. No m5/m30/w1 — those only via history
data_quality + freshnesswhether to trust the snapshot: 0…100 score plus freshness of each component
macro_event_soontrue = major event (FOMC/CPI) within 60 min — better pause
derivatives + liquidationsfunding, open interest, liquidation cascades — a filter, not a signal
macro_snapshotFear & Greed, BTC dominance, stables, dollar index, 10Y yield
cross_exchangemid cross-check; divergence_warning = don't enter unchecked
market_regime / volatility_regimemarket backdrop: BTC correlation, volatility, breadth
Limitation: context_scope="live_snapshot", historical_safe=false — this is a current snapshot, not historical data for backtesting. Never blend it into a backtest.
# Minimum gate before entry (pseudocode — working examples below) bar.closed == true ready == true and indicators_ready == true data_quality.score >= 70 and lag_sec <= 120 cost_risk.tradable == true macro_event_soon == false and divergence_warning == false

You can pass your own fees as query params — then tradable is computed for your cost model: ?fee_buy=0.001&fee_sell=0.001&slippage=0.0002&k_sl=2&rr=2&tradable_c_r_max=0.45. Backtest and live must use the same fees, otherwise the "expensive / fine" verdict diverges.

How to read trade_cost / cost_risk: fees + half-spread + slippage = c_spot (fraction of the trade, 0.0025 = 0.25% round-trip). Divide by the stop (k_sl × ATR) → c_r. A value of 0.45 means costs eat almost half a stop; p_be is the win rate needed to break even at your RR. tradable: false — the pair is too expensive for your stop: widen the stop or skip.

Response example (trimmed, same structure)

The full ~80-field JSON with every block explained lives in agents.md §7. Below is the skeleton returned by GET /v1/context/BTCUSDT:

# GET /v1/context/BTCUSDT → 200, live_snapshot (trimmed) { "symbol": "BTCUSDT", "interval": "1m", "lag_sec": 75, "bar": { "ts": 1784179200000, "close": "65040.00", "closed": true, "indicators": { "ema20": "64910.12", "rsi14": "54.20", "atr14": "180.50", "adx14": "22.10", "vwap": "64980.00" } }, "data_quality": { "score": 92, "flags": [] }, "trade_cost": { "c_spot": "0.0025", "spread_from_depth": true }, "cost_risk": { "c_r": "0.446429", "p_be": "0.482143", "tradable": true }, "htf": { "m15": {…}, "h1": {…}, "h4": {…}, "d1": {…} }, "macro_event_soon": false, "volatility_regime": "low_vol_range" }
How to read: score ≥ 70 + tradable: true + macro_event_soon: false = the snapshot is tradable. The signal comes only from bar (closed: true), everything else is filters.

History & timeframes

Oracle maintains native higher-timeframe candles itself — don't stitch 5m/15m from minute bars on the client (you'd lose ADX, VWAP, CVD, OBV, MFI).

IntervalDepthIndicators
1m~60 daysyes
5m60 daysyes
15m90 daysyes
30m120 daysyes
1h~9 monthsyes
4h2 yearsyes
1d3 yearsyes
1w5 yearsyes
# Single-pair history and a multi-pair batch GET /v1/history/BTCUSDT?interval=15m&limit=300 POST /v1/history/batch {"symbols":["BTCUSDT"], "interval":"15m", "limit":200}

Order book: live and history

Two different entities — don't mix them up:

GET /v1/depth (live)Microstructure (history)
Whatcurrent top-20, up to 10 seconds windowclosed UTC minute: spread, imbalance, walls, pressure
Whyentry validation 1–2 s after the signalbacktests and book filters on history
Wherecurrent window only, not stored as historyhistory + microstructure_close WS event
Call/v1/depth/BTCUSDT?seconds=3/v1/microstructure/BTCUSDT / /latest
Gate for a book-based entry: sample_count ≥ 40, coverage_pct ≥ 66.7, quality.score ≥ 70, microstructure.ts == bar.ts, closed == true. Live depth is an execution filter — it never rewrites the historical signal of the closed bar.
Book and CVD are not always available. On failover (failover: true) candles may stay valid, but minute microstructure may stop and cvd.status go down. For book and CVD strategies a plain status == "ok" is not enough: check bars.status (candles) and cvd.status (flow) in GET /health.

Indicators

The same set on 1m and on every higher TF after warmup. During warmup individual fields are null (don't substitute 0). Each group below is a ready input for a bot, entry signaler, or screener: trend, strength, momentum, volatility, volume, price anchor, candle quality, levels.

GroupKeysWhat it shows and how to use it
Trendema20 / ema50 / ema200, ema50_slope_pctDirection and speed. Price above EMA200 — bullish backdrop; ema20/ema50 cross — momentum; |slope| ≤ 0.1 — grid-mode flat
Strength & directionadx14, plus_di14 / minus_di14 / di_sideADX is strength only (flat ≤25, trend ≥30), DI is the side (di_side = 1/-1/0). Trend bots trade only with ADX ≥ 25
Momentumrsi14, mfi14Overheat 0–100 (>70 overbought, <30 oversold). MFI is the volume-weighted second vote. Don't fade a trend on RSI alone
Volatilityatr14 / atr_pct, atr50 / atr_ratio_14_50, bb_mid / bb_upper / bb_lower / bb_widthStop = k×ATR; ratio <0.8 — squeeze (energy builds), >1.2 — expansion; BB-width is the squeeze meter for breakouts
Volume & flowvol_sma20, volume_usd / volume_rate, obv, cvd_deltaConfirm entries with ≥1.5–2× normal $/min (comparable across TFs). Cumulative CVD is a client-side sum of cvd_delta; valid only while cvd.status == ok
Price anchorvwapSession VWAP (resets at 00:00 UTC): above — buyers' day. On 1w it is not a weekly VWAP
Candle shapebody, wick_balance, close_pos, candle_q, range_ratioBar quality as one number candle_q; close_pos > 0.7 — strong long bar; range_ratio << 1 — noise, skip
Levelsswing_high / swing_low, last_swing_*Strength-2 fractals with a 2-bar lag: stops beyond the extreme, targets, breakout entries. swing_* only on the confirmation bar

Core readiness: indicators_ready means ema20+rsi14+atr14 are present. The rest arrives later: ADX/DI after ~27 bars, EMA200 after 200 bars of that interval, Bollinger after 20. A full reference for every key (formula, thresholds, bot usage, pitfalls) lives in agents.md. What Oracle will never have: MACD, Stochastic, Supertrend, Ichimoku — that's your strategy's logic.

How to combine the fields

Oracle data is inputs, not a ready-made buy/sell. Three working combinations for a bot, entry signaler, or screener:

Trend + HTF

On 1m bar_close check context.htf.h1: price above ema50, adx14 ≥ 25 and di_side == "1" — only longs on the lower TF. Below H1 EMA200 — longs only with breakout confirmation. Cuts entries against the higher trend.

Mean-reversion

On your TF: rsi14 left the zone (>70 down or <30 up), price returned to bb_mid / vwap, volume_rate at least normal, tradable, no news within 60 min. Without the quality gate skip the signal.

Screener + book

Every 5 min rank pairs by volume_rate and atr_ratio_14_50 (squeeze + volume). 1–2 s after a signal call GET /v1/depth?seconds=3: spread OK, imbalance with the trade, microstructure gate. No — skip; yes — limit at the wall.

Starter TF: native 15m + H1 filter. 1m without experience is chop and fees. Cumulative CVD is a client-side sum of cvd_delta on closed bars; backtests use history as-of, never live context.

Plans

Three client plans: free, basic, pro. Prices live on the key-sales site; here are the limits so you can pick a plan for your bot. Your key's exact numbers are always in GET /v1/me: if they differ from the table, trust /v1/me.

PlanRequests / dayRPMMin. pauseRec. pauseMax WSFor
free2,000302000 ms2000 ms1learn the API, one bot
basic5,00060200 ms1000 ms2live 24/7: 1–2 bots per key
pro8,00090150 ms667 ms3live 24/7: up to 3 bots on one key
Practice: on basiccontext every 60–90 s per active pair + always before entry; on free — roughly every ~30 min. latest for tickers — every 10–15 s, history — cached 5+ min. A bot without WS polling latest every 10 s burns ~8,640 requests a day — no plan covers that: switch to WebSocket first.

Example headers after a billable GET /v1/context/BTCUSDT (names are case-insensitive, exposed in CORS):

X-Quota-Used: 150 X-Quota-Limit: 2000 X-Quota-Remaining: 1850 X-Quota-Cost: 1 X-Quota-Weight: 3 X-Quota-Reset: 1784217600

GET /v1/me returns the same headers but does not spend daily quota. On 429 read Retry-After and JSON retry_after_ms; a normal 200 has no Retry-After. Full field table: agents.md, «Заголовки лимитов» / quota headers.


All endpoints

Service

MethodPathDescription
GET/healthservice status and bar freshness (public)
GET/v1/mekey plan, quota left, expires_at

Symbols and status

MethodPathDescription
GET/v1/symbolsactive pairs + bars / ready / history
GET/v1/status/{symbol}lag, ready, indicators, quality for one pair
GET/v1/meta/{symbol}tick / step / minNotional for order rounding

History and sync

MethodPathDescription
GET/v1/history/{symbol}?interval=1m|5m|15m|30m|1h|4h|1d|1w, limit (up to 10080), from/to in ms
POST/v1/history/batchup to 20 pairs in one request, same interval set
POST/v1/sync/bootstrapcold dump of 1m only (up to 10080 bars per pair)
GET/v1/latest?symbols=last closed bar + ticker (up to 50 pairs; always read via .symbols[PAIR])
GET/v1/indicators/{symbol}indicators only, 1m only (higher TFs — history)

Market slice

MethodPathDescription
GET/v1/context/{symbol}full decision snapshot
GET/v1/depth/{symbol}live top-20 (?seconds=0..10), execution filter
GET/v1/derivatives/{symbol}funding, OI, liquidations
GET/v1/calendarmacro events: ?hours=, ?high_only=true, for backtests ?as_of=bar.ts&window_min=60
GET/v1/macroFear & Greed (+history ?fng_days=), dominance, stables, DXY/10Y
GET/v1/quotes/{symbol}BBO cross-check
GET/v1/market-regimeBTC correlations, volatility, market breadth

Microstructure

MethodPathDescription
GET/v1/microstructure/{symbol}closed minute book history (interval=1m, up to 10080)
GET/v1/microstructure/{symbol}/latestlast closed minute

Code examples

Two starter clients — Python and Node.js. The node address is already filled in; add your mo_… key. Switch languages via tabs.

REST: first request and pre-entry gate

# Standard library only import json, urllib.request BASE = "https://api.market-oracle.pro" KEY = "mo_…" def api(path: str): req = urllib.request.Request(BASE + path, headers={"Authorization": f"Bearer {KEY}"}) with urllib.request.urlopen(req, timeout=15) as r: return json.load(r) health = api("/health") assert health["status"] == "ok" and health["bars"]["status"] == "ok" me = api("/v1/me") print(me["tier"], "remaining:", me["daily_remaining"]) ctx = api("/v1/context/BTCUSDT") bar = ctx["bar"] ok = (bar["closed"] and ctx["ready"] and ctx["indicators_ready"] and ctx["data_quality"]["score"] >= 70 and ctx["lag_sec"] <= 120 and ctx["cost_risk"]["tradable"] and not ctx["macro_event_soon"]) print("may analyze:", ok, bar["close"])

WebSocket: listen to closed bars

# pip install websockets import asyncio, json import websockets async def main(): async with websockets.connect( "wss://api.market-oracle.pro/v1/stream", extra_headers={"Authorization": "Bearer mo_…"}, ) as ws: print(await ws.recv()) # hello await ws.send(json.dumps({"op": "subscribe", "symbols": ["BTCUSDT"]})) seen = set() async for msg in ws: ev = json.loads(msg) if ev.get("type") != "bar_close": continue key = f"{ev['symbol']}:{ev['bar']['ts']}" if key in seen: continue seen.add(key) print("bar closed:", key, ev["bar"]["close"]) asyncio.run(main())

Errors

Check the HTTP status first, then the machine code field (equals status on errors) in the body. Never parse the error text.

code / statusHTTPWhen and what to do
ok200success
key_missing / key_invalid / key_expired / key_revoked401key problem: stop trading, ask the operator for a renewal
too_fast / rate_limited429requesting too fast: wait retry_after_ms + jitter, then slow down
quota_exceeded429daily quota spent: don't retry data REST until UTC midnight; WS and utility endpoints keep working
ws_limit429*too many WS connections per key: read code (status may be missing)
unavailable503Oracle temporarily unavailable: backoff and retry

Useful headers: daily remainder is X-Quota-* (not X-RateLimit-*); on 429 use Retry-After and JSON retry_after_ms. Unknown symbol / bad range — 400, internal error — 5xx with bounded exponential backoff.

Checklist for an AI agent / bot

  1. GET /healthstatus == "ok" and bars.status == "ok"; for CVD strategies also cvd.status == "ok".
  2. GET /v1/me → account for plan, daily remainder, rpm, min_interval_ms, expires_at.
  3. GET /v1/symbols → only ready == true, prefer full_day.
  4. WS subscribe (for the book — microstructure: true); dedupe both event types by symbol:ts.
  5. On bar_close — analyze once. 5m/15m/1h signals — only on the closed native bar of your TF.
  6. GET /v1/context/{symbol} on schedule and before entry; gates: score ≥ 70, lag ≤ 120, cost_risk.tradable, no macro_event_soon or divergence_warning.
  7. 1–2 s after the signal — optionally GET /v1/depth?seconds=3 as an execution filter.
  8. After reconnect: pause ≥ min_interval → /v1/history (+ microstructure) → exact join by ts.
  9. Don't recompute EMA/RSI/ADX on the client if Oracle already served them in bar.indicators. Never publish your mo_… key in public repos or frontend code.