Metadata-Version: 2.4
Name: cruscy
Version: 0.1.0
Summary: Python client for the cruscy data platform: Binance spot L2/tape SQL, quality index and backtests
Author: cruscy
License: MIT
Project-URL: Homepage, https://data.cruscy.com
Project-URL: Documentation, https://data.cruscy.com/docs-public
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Office/Business :: Financial
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: httpx>=0.24
Provides-Extra: pandas
Requires-Dist: pandas>=1.5; extra == "pandas"
Provides-Extra: test
Requires-Dist: pytest; extra == "test"
Requires-Dist: pandas>=1.5; extra == "test"
Dynamic: license-file

# cruscy

Python client for the [cruscy data platform](https://data.cruscy.com): Binance spot
full-depth order-book diffs (`l2_*`), the complete trade tape (`trades_*`), derived
streams, a daily quality index and a backtester — all served as computation over the
data (SQL, no file downloads).

```
pip install "cruscy[pandas]"     # pandas is optional; without it results are lists of dicts
```

## Quickstart (free key, demo hour 2026-08-18 12:00–13:00 UTC)

```python
import cruscy
c = cruscy.Client("crk_...")                       # or export CRUSCY_API_KEY=crk_...
print(c.catalog().head())                          # streams with first/last day
print(c.schema("trades_SOLUSDT"))                  # columns + meaning (field dictionary)
df = c.sql("SELECT ts, price, qty FROM trades_SOLUSDT "
           "WHERE day='2026-08-18' AND ts >= '2026-08-18 12:00' LIMIT 1000")
print(df.head(), df.attrs["elapsed_ms"], "ms")
print(c.quality("2026-08-18"))                     # Q index, book rebuild check, tape vs exchange
l2 = c.sql("SELECT * FROM l2_SOLUSDT WHERE day='2026-08-18' "
           "AND ts BETWEEN '2026-08-18 12:10' AND '2026-08-18 12:16'")
print(cruscy.rebuild_book(l2)["bid"], cruscy.rebuild_book(l2)["ask"])
```

`ts`, `price` and `qty` are decoded convenience columns the SQL engine adds on top
of the raw `timestamp_ms`, `price_int` (× 10^2) and `volume_int` (× 10^8).

## API

| Call | Returns |
|------|---------|
| `Client(api_key=None, *, base_url, timeout=60, retry=True, max_retries=3)` | client; key falls back to `CRUSCY_API_KEY` |
| `c.catalog(all=False)` | DataFrame indexed by stream (`tier, kind, desc, first_day, last_day, n_days, avg_day_bytes, last_day_bytes, has_gaps`, plus `available_on` for out-of-plan streams with `all=True`) |
| `c.schema(stream)` | DataFrame `name, type, desc`; `notes`, `conventions`, `dictionary_version` in `df.attrs` |
| `c.sql(query, raw=False)` | DataFrame (`truncated`, `elapsed_ms`, `days_used` in `df.attrs`); `raw=True` gives the JSON payload |
| `c.quality(day=None)` | dict for that day from `/status.json`, or the list of all days |
| `c.status()` | full `/status.json` (`generated_at`, `days`, `incidents`) |
| `c.backtest(code, symbols, date_from, date_to, *, name="sdk", capital=236, regime_report=True)` | `Run` |
| `run.wait(timeout=600, poll=5)` | the same `Run` once `done` (raises on `failed`/timeout) |
| `run.status()` / `run.report()` / `run.equity()` | run row / report dict (`None` until finished) / DataFrame `ts, eq, hold` |
| `c.runs()`, `c.run(id)`, `c.save_strategy(name, code)` | run list / handle / `{strategy_id, version, lint}` |
| `cruscy.rebuild_book(df_l2, at_ts_ms=None, depth=10)` | `{bid, ask, bids, asks, last_update_id, snapshot_id, ts_ms, frames, stopped}` |

Errors are raised as `cruscy.CruscyError(code, message, hint, status)` mirroring the
server envelope `{"error": {"code", "message", "hint"}}`. HTTP 429 is retried
automatically up to 3 times honoring `Retry-After` (disable with `retry=False`).

### Backtest

```python
code = '''
class Strategy:
    def analyze(self, symbol, data):
        ...
'''
run = c.backtest(code, symbols=["SOL/USDT"], date_from="2026-08-18", date_to="2026-08-18", name="demo")
report = run.wait().report()
curve = run.equity()
```

### Rebuilding the order book

`rebuild_book` follows the recipe from the field dictionary: take the latest
`is_snapshot = 1` frame at or before the moment, then apply `is_snapshot = 0` frames in
`(epoch, ingest_seq)` order, skipping frames with `final_id <= snapshot id` and
requiring `first_id == previous final_id + 1` (the first diff after a snapshot may
straddle it, `first_id <= snapshot id + 1 <= final_id`, as in Binance's own recipe).
`volume_int = 0` removes a level. If continuity breaks the result carries
`stopped="gap"` and the book as of the last good frame. See
`examples/book_rebuild_demo.py` for a check against the independently recorded
top of book (`ob_*`).

## License

MIT
