quantada 0.1.0
Production-grade Python library for acquiring, storing, transforming, and validating market data
License: MIT
Requires Python: >=3.10
Keywords: quantitative, finance, market-data, trading, backtesting, crypto

# Quantada

**Simple, unified interface for crypto market data from multiple sources**

[![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)

---

## Quick Start
---

### Installation
```bash
pip install quantada
```

### First Request
```python
import quantada as qd

# Fetch 30 days of hourly BTC/USDT data from Binance
data = (
    qd.data("BTCUSDT", source="binance", category="spot")
    .last(30)
    .resolution("1h")
    .get()
)

print(data.head())
#                        open      high       low     close      volume
# timestamp
# 2024-11-01 00:00:00  64000.0  64120.5  63810.2  64050.3  1250.0
# 2024-11-01 01:00:00  64050.3  64200.1  63950.0  64110.7  1188.3
```

---

## Core API Reference

### `qd.data(symbols, source=None, category=None)`

Main entry point for data queries. Returns a chainable query builder.

**Parameters:**
- `symbols` (str | list[str]) - Single symbol or list of symbols
- `source` (str, optional) - Data source: `"binance"`, `"bybit"`, `"local"`, or a file path (csv/parquet)
- `category` (str, optional) - Market category for crypto sources, e.g. Binance (`"spot"`, `"usdm"`, `"coinm"`) or Bybit (`"spot"`, `"linear"`, `"inverse"`, `"option"`)

**Returns:** `SymbolQuery` or `MultiSymbolQuery`

---

### Query Builder Methods

#### **Time Range**

```python
.between(start, end)          # Explicit date range
  # start/end: str or datetime
  # Example: .between("2024-01-01", "2024-12-01")

.since(start)                 # From start date to now
  # Example: .since("2024-01-01")

.last(n, unit="days")         # Last N time units
  # unit: "days" | "hours" | "minutes"
  # Example: .last(24, "hours")

.recent(bars)                 # Last N bars (time inferred from resolution)
```

#### **Resolution**

```python
.resolution(interval)         # Set bar resolution
  # interval: "1m", "5m", "15m", "1h", "4h", "1d", "1w", "1M"
```

#### **Data Cleaning**

```python
.clean(
    remove_outliers=False,
    remove_invalid_prices=False,
    validate_ohlc=False,
    dropna_subset=None,
    dropna_how='any',
)

# Convenience transforms
.fill_forward()
.fill_backward()
.interpolate(method='linear')
.resample(to_timeframe)
.transform(func)              # Custom transform
```

#### **Execution**

```python
.get(format="dict", parallel=True, workers=4)  # format: "dict" or "combined"
```

---

## Examples by Feature

### 1. Different Data Sources
```python
import quantada as qd

# Binance (crypto spot)
binance = qd.data("BTCUSDT", source="binance", category="spot").last(7).resolution("1h").get()

# Bybit (crypto perpetuals)
bybit = qd.data("ETHUSDT", source="bybit", category="linear").last(14).resolution("4h").get()

# Live stream (Binance)
stream = (
    qd.stream(["BTCUSDT", "ETHUSDT"], source="binance", category="spot")
    .resolution("1s")
    .on_tick(lambda df: print(df.head()))
    .get()
)
stream.start()
# ... later ...
stream.stop()
```

### 2. Time Ranges
```python
# Specific date range
btc = qd.data("BTCUSDT", source="binance", category="spot").between("2024-01-01", "2024-02-01").resolution("1d").get()

# Last 24 hours
eth = qd.data("ETHUSDT", source="binance", category="spot").last(24, "hours").resolution("1h").get()

# Last 90 days
sol = qd.data("SOLUSDT", source="bybit", category="linear").last(90).resolution("1d").get()
```

### 3. Different Resolutions
```python
# 1-minute bars
btc_1m = qd.data("BTCUSDT", source="binance", category="spot").last(6, "hours").resolution("1m").get()

# 15-minute bars
eth_15m = qd.data("ETHUSDT", source="binance", category="spot").last(12, "hours").resolution("15m").get()

# 4-hour bars
sol_4h = qd.data("SOLUSDT", source="bybit", category="linear").last(14).resolution("4h").get()

# Daily bars
bnb_1d = qd.data("BNBUSDT", source="binance", category="spot").last(60).resolution("1d").get()
```

### 4. Data Cleaning
```python
clean_data = (
    qd.data("BTCUSDT", source="binance", category="spot")
      .last(30)
      .resolution("1h")
      .clean(remove_invalid_prices=True, validate_ohlc=True, remove_outliers=True)
      .get()
)
```

### 5. Multiple Symbols
```python
data_dict = (
    qd.data(["BTCUSDT", "ETHUSDT", "SOLUSDT"], source="binance", category="spot")
      .last(14)
      .resolution("4h")
      .get(parallel=True, workers=4)
)
# {"BTCUSDT": DataFrame, "ETHUSDT": DataFrame, ...}

combined = (
    qd.data(["BTCUSDT", "ETHUSDT"], source="binance", category="spot")
      .last(30)
      .resolution("1d")
      .get(format="combined")
)
# Access specific symbol: combined.loc["BTCUSDT"]
```

### 6. Advanced Transforms
```python
import quantada.transforms as qdt

data = (
    qd.data("BTCUSDT", source="binance", category="spot")
      .last(7)
      .resolution("1m")
      .clean(remove_invalid_prices=True)
      .fill_forward()
      .resample("1h")
      .transform(lambda df: df[df['volume'] > 10_000])
      .get()
)

# Functional approach
df = qd.data("ETHUSDT", source="binance", category="spot").last(30).resolution("1h").get()
df = qdt.remove_duplicates(df, keep="last")
df = qdt.remove_outliers(df, columns="close", method="iqr")
df = qdt.fill_forward(df)
```
---

## Configuration

```python
import quantada as qd

# Set default data directory and storage type
qd.config(
    data_dir="./market_data",
    store_type="parquet",  # or "csv", "sqlite", "duckdb"
    cache=True,
    validate=True,
)
```

---

## Data Model

All data returned as pandas DataFrame with DatetimeIndex:

```
                       open      high       low     close    volume
timestamp
2024-01-01 00:00:00  42000.0  42100.5  41850.2  42080.3  1580.0
2024-01-01 01:00:00  42080.3  42210.1  41990.0  42150.7  1490.4
```

**Columns:**
- `open` - Opening price
- `high` - Highest price
- `low` - Lowest price
- `close` - Closing price
- `volume` - Trading volume

---

## Live Streaming

Quick facts:
- Sources: Binance (`spot`, `usdm`, `coinm`) and Bybit (`spot`, `linear`, `inverse`)
- Symbols: Binance uses `BTCUSDT`; Bybit inverse uses `BTCUSD`, spot/linear use `BTCUSDT`
- `resolution()` selects kline streams when supported; `.resample()` auto-buffers and aggregates client-side

Supported sources (live):

| Source  | Categories           | Symbol examples              |
|---------|----------------------|------------------------------|
| binance | spot, usdm, coinm    | BTCUSDT, ETHUSDT, SOLUSDT    |
| bybit   | spot, linear, inverse| BTCUSDT (spot/linear), BTCUSD (inverse) |

Ticks (push):
```python
import quantada as qd

stream = (
    qd.stream(["BTCUSDT", "ETHUSDT"], source="binance", category="spot")
    .resolution("1s")               # trades at 1s cadence
    .on_tick(lambda df: print(df.tail(1)))
    .get()
)
# runs until stopped
```

Bars (auto-buffered/resampled):
```python
stream_1m = (
    qd.stream(["BTCUSDT"], source="binance", category="usdm")
    .resample("1m")                       # kline if available; otherwise buffer+resample
    .clean(remove_outliers=True)          # optional post-aggregate cleaning
    .on_tick(lambda df: print(df.tail(1)))# bar callback
    .get()
)
```

Pull style (no callback):
```python
session = qd.stream("BTCUSDT", source="binance", category="spot").get(start=True)
if session.has_new_data:
    latest = session.consume_latest()
```

Common pitfalls:
- `resolution()` alone does not downsample trade feeds; for bars, use kline-capable intervals or add `.resample(...)`.
- Bybit categories require matching symbols (`BTCUSD` for inverse, `BTCUSDT` for spot/linear); Binance uses the same symbol format across categories.
- Missing data locally? Ensure you’ve fetched and written bars before reading from `source="local"`.

Troubleshooting live streams:
- Deprecation warnings from upstream `websockets` in tests can be ignored; they do not affect functionality.
- Empty data from live sources often means wrong symbol/category or network issues; double-check symbols and categories above.

---

## License

MIT License - see LICENSE file for details