# TickFeed API — Developer Guide

Read-only forex and precious-metals market data (live prices and OHLC candles) for trading bots, backtests and AI agents, over **REST** and **MCP**. You don't need MetaTrader 5 or a broker connection; you only need an API key and HTTP.

> Base URL: **`https://api.tickfeed.pro`**. Landing page and sign-up: `https://tickfeed.pro`.

## 1. Quickstart

```bash
curl -H "X-API-Key: $MDP_API_KEY" https://api.tickfeed.pro/v1/price/XAUUSD
```

```python
# pip install ./clients/python      (standard library only)
from marketdata_client import MarketDataClient

md = MarketDataClient("https://api.tickfeed.pro")           # reads MDP_API_KEY
p = md.price("XAUUSD")
print(p["bid"], p["ask"], p["freshness"])                # always check freshness
bars = md.candles("XAUUSD", "M5", limit=100)["candles"]  # oldest first
closed = [b for b in bars if b["complete"]]              # ignore the forming bar
```

Interactive reference: `https://api.tickfeed.pro/docs` (OpenAPI).

## 2. Authentication

* **Getting a key:** choose a plan on the landing page (`/`). After checkout your key is shown **once**; copy it then.
* **Sending it:** put it in **`X-API-Key: mdp_…`** (REST and MCP). The MCP endpoint also accepts **`Authorization: Bearer mdp_…`**.
* **Storage:** only a hash of each key is stored. Keep yours out of source control and logs; if one leaks, contact support to rotate it.
* **User-Agent:** send a descriptive `User-Agent` header (e.g. `mybot/1.0`). The edge's bot protection rejects some generic library defaults, such as Python `urllib`'s, with HTTP 403. The official Python client sets one for you; curl and `requests` work as they are.
* **No key needed** for `/`, `/status`, `/health`, `/plans`, `/docs` and `/developer-guide`.
* **Managing your subscription:** `POST /v1/billing/portal` with your key returns a Stripe link where you can upgrade, downgrade, cancel or update payment details.

## 3. Symbols

| Public symbol | Session schedule | Notes |
|---|---|---|
| `XAUUSD` | `metals`: Sun 18:00 → Fri 17:00 New York, daily break 17:00–18:00 NY | Gold vs USD, 2 decimals, point 0.01 |
| `EURUSD` | `forex`: Sun 17:00 → Fri 17:00 New York; quiet around the 17:00 rollover | 5 decimals, point 0.00001 |

* Use the **public symbol** in paths (case-insensitive). The response also shows the upstream `broker_symbol`; brokers name instruments differently (`XAUUSD.m`, `GOLD`, …), and the service maps them for you.
* `GET /v1/symbols` is the source of truth for what is available, with precision (`digits`, `point`) and currencies.
* Exchange holidays are not modelled yet.

## 4. Timeframes and candles

Timeframes: `M1 M5 M15 M30 H1 H4 D1`.

`GET /v1/candles/{symbol}?timeframe=H1&limit=100[&start_time=…][&end_time=…][&source=mt5|db]`

| Behaviour | Detail |
|---|---|
| Order | Always chronological, oldest first |
| `time` | Bar **open** time, UTC, ISO 8601 (`2026-10-12T22:00:00Z`) |
| `complete` | `false` = the bar's period hadn't ended when captured; values can still change. Usually only the newest bar. |
| Gaps | Never filled, interpolated or forward-filled. Weekends, daily breaks and holidays produce gaps. `GET /v1/candles/{symbol}/gaps` classifies them (`expected: true` = scheduled closure). |
| D1 / H4 alignment | Bars align to the broker's day, which starts at 17:00 New York (21:00 UTC in summer, 22:00 UTC in winter). A D1 bar's `time` is therefore 21:00 or 22:00 UTC. |
| `limit` | 1–5000 per request (MCP: 1–1000) |
| Without `start_time` | The latest `limit` bars up to `end_time` (default now) |
| With `start_time` | The first `limit` bars from `start_time`. If more exist: `truncated: true` and `next_start_time` = the `start_time` for the next page (the Python client's `iter_candles()` follows this for you) |
| Range cap | A range covering more than 100 000 bars is rejected (`range_too_large`); use pages or a larger timeframe |
| Naive times | Treated as UTC |
| `spread_points` | Spread recorded for the bar, in broker points |
| `source` | `mt5` (default) = straight from the feed; `db` = the service's stored history |
| Empty range | `200` with `count: 0`, not an error |

## 5. Prices and freshness

`GET /v1/price/{symbol}` → `bid`, `ask`, `spread` (price units), `spread_points` (points), `point`, `tick_time` (UTC), `age_seconds`, `freshness`, `market_open`.

| `freshness` | Meaning | What your bot should do |
|---|---|---|
| `fresh` | Tick ≤ 120 s old | Use it |
| `market_closed` | Older, but the market is outside its schedule (or within 5 min after an open / 2 min before a close) | Don't treat it as live; wait for the session |
| `stale` | Older **while the market should be trading**: a feed problem | Pause trading decisions; check `/health` |

The last known price is always returned, labelled, and never presented as live when it isn't.

## 6. Health

`GET /health` → `status` (`ok` | `degraded` | `down`), `reasons`, per-symbol `feeds` (freshness, `market_open`), and collector status. `down` returns HTTP 503. Poll it (no key needed) to decide whether to trust the data. A closed market is `ok`, not `degraded`.

## 7. Errors

All errors use one shape:

```json
{"error": {"code": "unsupported_symbol", "message": "Symbol 'BTCUSD' is not supported. Supported: XAUUSD, EURUSD."}}
```

| HTTP | `code` | Retry? | Typical cause |
|---|---|---|---|
| 401 | `unauthorized` | No | Missing, wrong or revoked key |
| 403 | `account_inactive` | No | Subscription suspended or canceled |
| 403 | `plan_restriction` | No | Your plan doesn't include this (e.g. MCP) |
| 429 | `quota_exceeded` | After `Retry-After` (next 00:00 UTC) or upgrade | Daily quota used up |
| 429 | `ip_rate_limited` | Yes, after `Retry-After` | Too many requests from one address |
| 404 | `unsupported_symbol` | No | Symbol not offered |
| 422 | `invalid_parameters`, `invalid_range`, `range_too_large` | No (fix the request) | Bad timeframe/limit/time, start after end, range too big |
| 429 | `rate_limited` | Yes, after `Retry-After` seconds | Over your per-minute limit |
| 503 | `upstream_unavailable` | Yes, with backoff | Feed temporarily disconnected |
| 503 | `data_unavailable` | Yes, with backoff | Feed connected but no tick/history yet |
| 503 | `upstream_symbol_missing` | No (contact us) | Service-side symbol misconfiguration |

The Python client raises `MarketDataError` (`.status`, `.code`). It also retries 429 (honouring `Retry-After`), 503 and connection errors with exponential backoff (`max_retries`, default 3).

## 8. Rate limits, quotas and usage

| Plan | Requests/minute | Requests/day | MCP |
|---|---|---|---|
| Free developer | 30 | 1 000 | yes |
| Developer | 120 | 50 000 | yes |
| Professional | 600 | 500 000 | yes |

(Current limits are listed at `GET /plans`; prices are shown at checkout.)

* **Per-minute limit:** a sliding 60-second window across all your keys. **Daily quota:** resets at 00:00 UTC.
* **Headers** on every response:
  * `X-RateLimit-Limit` / `X-RateLimit-Remaining` (per minute);
  * `X-Quota-Limit` / `X-Quota-Remaining` (per day).
* **Over a limit:** `429` with `Retry-After` (`rate_limited` or `quota_exceeded`).
* **What counts:** REST and MCP share the same budget. Each MCP HTTP request counts (`initialize`, `tools/list` and each `tools/call`).
* **`GET /v1/usage`:** your plan, limits, today's count, remaining quota and the last 30 days.
* **Bots:** poll at the cadence of the bars you trade (e.g. every 30 s for M5), not in a tight loop.

## 9. MCP (for AI agents)

Endpoint: **`POST https://api.tickfeed.pro/mcp`**, MCP over Streamable HTTP (stateless, JSON responses). It uses the same service as REST, so results are identical. All tools are read-only (`readOnlyHint: true`) and return structured output.

| Tool | Arguments | Returns |
|---|---|---|
| `list_symbols` | – | Symbols, sessions, precision, timeframes |
| `get_price` | `symbol` | Same as `/v1/price` |
| `get_candles` | `symbol`, `timeframe` (default M5), `limit` (≤1000), `start_time?`, `end_time?` | Same as `/v1/candles` |
| `get_feed_health` | – | Same as `/health` |
| `find_gaps` | `symbol`, `timeframe`, `start_time?`, `end_time?` | Same as `/v1/candles/{symbol}/gaps` |

The server's `instructions` tell the model how to read freshness, incomplete bars and gaps. Failures come back as tool errors (`isError: true`) with the same `code` names as REST.

**Claude Code** (syntax not verified on this server, where the CLI isn't installed):

```bash
claude mcp add --transport http tickfeed https://api.tickfeed.pro/mcp --header "Authorization: Bearer mdp_your_key"
```

**Any MCP client / Python SDK:** see `examples/mcp_client.py` (official `mcp` ≥ 2 SDK, Bearer header).

**Claude via the API without MCP:** see `examples/agent_claude.py`. It uses the Anthropic SDK tool runner with tools that call the REST API.

MCP only gives an agent standard access to the data. It doesn't add trading skill; your strategy and risk controls remain yours.

## 10. Python client reference (`clients/python`)

| Method | Notes |
|---|---|
| `MarketDataClient(base_url, api_key=None, timeout=15, max_retries=3)` | `api_key` defaults to `MDP_API_KEY` |
| `.price(symbol)` | dict as in §5 |
| `.candles(symbol, timeframe="M5", limit=100, start_time=None, end_time=None, source="mt5")` | One page; datetimes or ISO strings |
| `.iter_candles(symbol, timeframe, start_time, end_time=None, page_size=5000)` | Yields every bar in the range across pages |
| `.candles_df(..., completed_only=True)` | pandas DataFrame indexed by UTC time (needs pandas) |
| `.gaps(...)`, `.symbols()`, `.health()` | `health()` returns the body even when `down` |
| `parse_time(s)` | API timestamp → aware UTC `datetime` |

## 11. Examples (`examples/`)

| File | Shows |
|---|---|
| `quickstart.py` | Price with freshness label, last 10 candles |
| `bot_sma_signal.py` | Bot skeleton: decides only on completed bars, only when fresh, once per bar. Prints signals and **places no orders** |
| `download_history.py` | Paginated history to CSV for backtesting, completed bars only, gaps reported (not filled) |
| `mcp_client.py` | Direct MCP calls with the official SDK |
| `agent_claude.py` | Claude agent (Anthropic SDK tool runner) analysing live data |

Install the example extras with `pip install -r examples/requirements.txt`.

## 12. What is not guaranteed (yet)

* Uptime and coverage are being measured, not promised. There is a single upstream feed with no redundancy.
* Tick-by-tick history is not available; candles only.
* Holidays and early closes are not modelled in the freshness or gap classification.
* The upstream can revise closed bars (rarely; e.g. the spread field). Stored history converges to the upstream within ~10 minutes for the last 72 h.
