Docs & manual
Three ways in: SQL, backtest, MCP. Everything runs on our side — you get results, not files. Below: the full path from first click to daily work, then the reference.
The full path, step by step
- Evaluate for free. No card, no crypto, no promises: register with your email — a magic link arrives, no password to invent. The free plan gives one full hour of every stream (2026-08-18 12:00–13:00 UTC), SQL queries and backtest minutes — enough to check the data quality and the engine with your own hands. That trial is our refund policy: you know exactly what you buy before you pay.
- Look around. Open the console, run a snippet from the library (candles from ticks, VWAP, CVD). Every stream has its own page with schema and live depth window — start at gold_trades_BTCUSDT.
- Run your first backtest. On /backtest press «New», insert the example strategy, pick a window and symbols, hit Run. The report shows FARM vs buy-and-hold, per-regime breakdown and every trade. Failed run? The report shows the exact error line — fixing costs nothing, rejected runs do not burn minutes.
- Connect your AI agent (optional, and honestly the best part): account → AI/MCP tab → copy the ready config for Claude/Cursor. Your agent gets catalog, SQL and the backtester as tools — it researches, you read reports. See MCP below.
- Upgrade when the free window is tight. In account press «Upgrade» on a plan — an invoice opens with the exact USDT amount. Payment is USDT TRC20 on our own gateway's checkout page; see Paying in USDT.
- Work daily. Quotas reset at UTC midnight; usage bars live in your account. Renewal adds 30 days to the later of «now» and your expiry — remaining days never burn. After expiry: 5 days of grace, then a soft drop to free. Your strategies and history stay.
- Ask through tickets. Support is built in, with an SLA per plan — every question leaves a trail, nothing gets lost in a chat.
Paying in USDT (TRC20)
- Press «Upgrade» in your account — the invoice shows the exact amount and opens our gateway's checkout page.
- Send the exact amount in USDT on the TRON network (TRC20) — not ERC20, not BEP20. Any TRC20-capable wallet or exchange withdrawal works.
- After network confirmation (typically 1–2 minutes) the gateway detects the payment and your plan activates automatically — no screenshots, no «contact admin».
Sent a wrong amount or the wrong network? Open a ticket with the transaction hash immediately — we see the chain and will sort out what is sortable. USDT transfers are irreversible, so double-check before sending. Full policy: Terms §4–5.
Quickstart (API)
No key yet? Download one free hour of SOL/USDT (L2 diffs, top of book, trades, 2026-08-18 12:00–13:00 UTC) and rebuild the order book yourself with the notebook — the same check our nightly job runs.
- Register — a magic link, no password.
- Create an API key in your account. It is shown once.
- Ask the catalog:
curl -H "X-API-Key: crk_…" https://data.cruscy.com/v1/catalog
SQL console
One statement per request, read-only, 30-second budget. The virtual column
day selects files, so always constrain it:
SELECT time_bucket('1 min', ts) AS m,
last(price) AS close,
sum(qty) AS vol
FROM gold_trades_ETHUSDT
WHERE day = '2026-08-10'
GROUP BY 1 ORDER BY 1
Free plan: one demo hour (2026-08-18 12:00–13:00 UTC; use day = '2026-08-18'). Paid plans: the window of your plan.
day BETWEEN is not supported — use day IN (…).
Data dictionary
Column types come from the files themselves (every stream page shows them);
the meaning of each column is on the stream page and in
/v1/schema/<stream>. The conventions below hold for every
stream — read them once, they save a wrong backtest.
Time
Every *_ms column is Unix epoch milliseconds, UTC. A day is a UTC day; the virtual column `day` (YYYY-MM-DD) selects files. timestamp_ms is the exchange event time (Binance `E`/`T`), recv_ms is the time our collector received the frame. recv_ms − timestamp_ms is the delivery latency (median ≈35 ms).
Prices and sizes are integers
Tick streams store price_int and volume_int as Int64. price = price_int / 10^price_decimals, size = volume_int / 10^vol_decimals. Both multipliers are written in the parquet file metadata of every file (currently price_decimals=2, vol_decimals=8). The SQL console exposes decoded `price` and `qty` as well.
`side` means different things in different streams
In l2_*: side = 0 is a bid level, side = 1 is an ask level. In trades_* and gold_trades_*: side = Binance isBuyerMaker — side = 1 means the buyer was the maker (the aggressor SOLD), side = 0 means the aggressor BOUGHT. Aggressive buy volume is therefore sum(qty) WHERE side = 0.
Connection lineage
epoch is the number of the WebSocket session; it increments on every reconnect and survives collector restarts. ingest_seq is a monotone counter of received frames inside the collector. Rows of one exchange frame share (epoch, ingest_seq). Sorting by (epoch, ingest_seq) reproduces the exact arrival order.
Gaps are marked, not hidden
In l2_*, gap = 1 marks the first frame after a detected break; gap_reason says why: `stream_gap:expected=<id>:first=<id>` (the exchange sequence jumped inside a live session), `epoch_start:epoch=<n>:first=<id>` (first frame of a new session — continuity across the boundary is unknowable), `replay_start:…` (first frame after journal replay on restart), `anchor` (a scheduled reference snapshot, see below). A day with zero gap rows had a continuous sequence end to end.
Snapshots and diffs
l2_* mixes two row types. is_snapshot = 1 rows are full-book references from REST (200 levels per side; first_id = final_id = lastUpdateId); is_snapshot = 0 rows are incremental diffs from `<symbol>@depth@100ms` where first_id/final_id are Binance U/u. A reference snapshot is written every 300 s (gap_reason = 'anchor') and on every (re)connect. volume_int = 0 in a diff removes the level.
How to rebuild the book at any moment
1) Take the latest is_snapshot = 1 frame at or before your moment (at most 5 minutes back) and load its levels. 2) Apply following is_snapshot = 0 frames in (epoch, ingest_seq) order, skipping frames with final_id <= snapshot id, and requiring first_id = previous final_id + 1 (otherwise stop: a marked gap). 3) volume_int = 0 deletes the level. Acceptance test we run: a full UTC day of SOL/USDT rebuilt this way matched the independently recorded top of book (ob_*) in 436 445 of 436 445 checks; BTC/USDT 861 901 of 861 901 asks, 861 898 bids.
schema_version in file metadata
3 (from 2026-08-21): the column set equals this dictionary; ob_* carries only WebSocket-path rows. 2 (2026-08-06 … 2026-08-20): same columns, but files of 2026-08-01…05 lack epoch, ingest_seq and gap_reason although also stamped 2. 1: July 2026, no recv_ms. Days before 2026-08-12 have known coverage losses — see the Q index on /status.
Rebuilding the book: three rules
Replay in (epoch, ingest_seq) order. (1) On an is_snapshot=1 frame, reset the book — but only if its final_id (= lastUpdateId) is not older than the last diff you applied: anchor snapshots are fetched over REST and can land in the journal behind diffs the WebSocket already delivered. (2) After a snapshot with lastUpdateId L, skip diffs with final_id <= L; the first diff with final_id > L is applied whole (deltas are absolute). (3) Quantity 0 removes the level. With these rules the nightly full-day replay matches ob_* at every recorded event (/status.json → days[].book_full_replay). The Python client implements them in cruscy.rebuild_book.
Dictionary version 3. Every day is verified two ways before it is served: the order book is rebuilt from the diffs and compared with the independently recorded top of book, and the trade tape is compared trade-by-trade with Binance's public archive. Results per day: /status.
Strategy contract
The backtest runner calls analyze on every minute of the tape.
Return None when there is no signal:
class Strategy:
def analyze(self, symbol, data):
if data.get("rsi") and data["rsi"] < 30:
return {"side": "BUY", "size_pct": 50,
"custom_tp": data["price"] * 1.006,
"custom_sl": data["price"] * 0.996}
return None
data['ladder'] is absent on purpose: the level ladder and smart
exits are live-bot machinery and are not part of backtest contract v1. Your
exits are your own custom_tp / custom_sl / hold.
That is a deliberate decision: you are testing YOUR logic, not borrowing
ours.
Allowed imports: numpy, pandas, math, statistics, collections. Anything else is rejected by the linter BEFORE the queue, so it costs you no minutes. Code runs in an isolated process with no network and no access to platform files.
How results are scored
FARM is measured IN COINS against buy-and-hold: 1.0 means you matched the
hold, above means you beat it. A strategy that never trades sits in cash and
honestly shows first_px/last_px — not 1.0.
MCP for AI agents
{"mcpServers": {"cruscy-data": {
"url": "https://data-mcp.cruscy.com",
"headers": {"Authorization": "Bearer crk_…"}}}}
Tools: catalog, stream schema, SQL query, backtest submit and report.
How a day becomes final
All times UTC. A day is sold as final only after the last step.
| When | What happens | Where you see it |
|---|---|---|
| 00:00 | The day ends; the collector keeps writing 5-minute files of the new day. | catalog last_day |
| 00:15–00:50 | Yesterday is sealed: files merged per stream, manifest written (rows, sha256, sequence holes), day uploaded to cold storage. | /v1/catalog n_days |
| 03:00 | Quality job: Q per symbol, 12 book probes, and the full-day replay — every order-book snapshot we recorded is recomputed from the L2 diffs and compared (event-exact, by lob_update_id). | /status, /status.json, MCP get_quality |
| 03:00, retried hourly | Tape check: our trades_* vs Binance's own public daily archive, trade by trade. Binance publishes the archive with a variable lag, so the check retries until it succeeds. | tape_vs_exchange |
| after the tape check | The day is final: finalized_at is set and never changes. Files of a final day are not rewritten. | /status.json → days[].finalized_at |
Until finalized_at is set, treat the day as provisional: data is complete and queryable, but the exchange cross-check has not run yet.
Limits and errors
Every error comes back in one envelope:
{"error": {"code": "daily_limit", "message": "…", "hint": "…"}}
| Code | HTTP | What it means |
|---|---|---|
| no_key | 401 | No X-API-Key or Bearer header |
| bad_key | 403 | Key not found or revoked |
| daily_limit | 429 | Daily quota spent; Retry-After = seconds to UTC midnight |
| rpm | 429 | Too many requests per minute |
| bytes_month | 402 | Monthly output volume spent |
| bt_minutes | 402 | Not enough backtest minutes for this run |
| day_out_of_plan | 403 | Backtest window reaches days your plan does not cover |
| busy | 503 | Two heavy requests of yours already run |
Rejected requests do not burn your daily quota — only served ones do.