# 🏔️ Vanta Crunch — Challenge Package

Predict short-term crypto returns. Beat the leaderboard.

## Quickstart

```bash
pip install vanta-challenge matplotlib  # matplotlib optional, for plots
```

### 1. Write a model

```python
from vanta.tracker import TrackerBase
from vanta.indicators import RSI, EMA, OrderBookImbalance, FundingSignal

class MyModel(TrackerBase):
    def __init__(self):
        super().__init__()
        self.rsi = RSI(period=14)
        self.ema_fast = EMA(period=5)
        self.ema_slow = EMA(period=20)
        self.obi = OrderBookImbalance(period=10)
        self.funding = FundingSignal(period=8)

    def tick(self, data: dict):
        super().tick(data)
        for candle in data.get("candles_1m", []):
            price = float(candle["close"])
            self.rsi.update(price)
            self.ema_fast.update(price)
            self.ema_slow.update(price)

        # Order book imbalance (if available)
        ob = data.get("orderbook")
        if ob:
            self.obi.update(ob["imbalance"])

        # Funding rate (if available)
        fund = data.get("funding")
        if fund:
            self.funding.update(fund["funding_rate"])

    def predict(self, symbol: str, horizon_seconds: int, step_seconds: int) -> dict:
        if not self.rsi.ready:
            return {"expected_return": 0.0}

        signal = 0.0

        # RSI + trend filter
        if self.rsi.value < 30 and self.ema_fast.value > self.ema_slow.value:
            signal += 0.003
        elif self.rsi.value > 70 and self.ema_fast.value < self.ema_slow.value:
            signal -= 0.003

        # Order book pressure boost
        if self.obi.ready:
            signal += self.obi.value * 0.002  # imbalance [-1,1] → ±0.002

        # Funding rate mean-reversion (negative = contrarian)
        if self.funding.ready:
            signal -= self.funding.value * 50  # e.g. 0.0001 × 50 = 0.005

        return {"expected_return": max(-0.01, min(0.01, signal))}
```

### 2. Backtest it

```python
from vanta.backtest import BacktestRunner

result = BacktestRunner(model=MyModel()).run(
    subject="BTCUSDT",
    start="2025-06-01",
    end="2026-01-01",
)
result.summary()  # formatted metrics table
result.plot()     # equity curve, drawdown, hit rate, scatter
```

### 3. Compare with other strategies

```python
from vanta.backtest import compare
from vanta.examples import MeanReversionTracker, TrendFollowingTracker

compare([
    ("Mean Reversion", MeanReversionTracker()),
    ("Trend Following", TrendFollowingTracker()),
    ("My Model", MyModel()),
], start="2025-06-01", end="2026-01-01")
```

### 4. Sweep parameters

```python
from vanta.backtest import sweep

sweep(
    model_fn=lambda period: MyModel(period=period),
    params={"period": [7, 14, 21, 28]},
    start="2025-06-01", end="2026-01-01",
)
```

## How It Works

- **Data**: Multi-timeframe OHLCV candles (1m, 5m, 15m, 1h) + order book depth + funding rates from Binance
- **Prediction**: Every 15 minutes, predict the **1-hour forward return**
- **Scoring**: `score = expected_return × actual_return`
- **History**: ~1 year of data available for backtesting (Jan 2025 → present)

## Scoring

```
score = expected_return × actual_return
```

| Scenario | Score | Meaning |
|----------|-------|---------|
| You predict +0.01, price goes up +0.005 | ✅ +0.00005 | Correct direction, good conviction |
| You predict +0.01, price goes down -0.005 | ❌ -0.00005 | Wrong direction, penalized |
| You predict 0.0 | 🤷 0.0 | No opinion, no gain, no loss |

**Direction AND magnitude matter.** See [docs/scoring-guide.md](docs/scoring-guide.md) for details.

## Indicators

Built-in stateful indicators — no pandas/numpy required:

```python
from vanta.indicators import SMA, EMA, RSI, MACD, BollingerBands, ATR, VWAP, Returns

rsi = RSI(period=14)
rsi.update(price)     # feed one value
rsi.value             # current reading (or None if not ready)
rsi.ready             # True once enough data accumulated
```

| Indicator | `.update()` | `.value` |
|-----------|-------------|----------|
| `SMA(period)` | `(price)` | Moving average |
| `EMA(period)` | `(price)` | Exponential average |
| `RSI(period)` | `(price)` | 0–100 scale |
| `MACD(fast, slow, signal)` | `(price)` | `(macd, signal, histogram)` |
| `BollingerBands(period, num_std)` | `(price)` | `(upper, middle, lower, width, percent_b)` |
| `ATR(period)` | `(high, low, close)` | Average true range |
| `VWAP()` | `(price, volume)` | Volume-weighted price |
| `Returns(period)` | `(price)` | % return over N periods |
| `OrderBookImbalance(period)` | `(imbalance)` | Smoothed bid/ask imbalance |
| `SpreadTracker(period)` | `(best_bid, best_ask)` | Spread in basis points |
| `FundingSignal(period)` | `(funding_rate)` | Smoothed funding rate |
| `BasisTracker(period)` | `(basis)` | Smoothed futures basis |

## Examples

Seven starter strategies in [`vanta/examples/`](vanta/examples/):

| Strategy | Approach |
|----------|----------|
| Mean Reversion | Fades deviation from SMA |
| Trend Following | EMA crossover momentum |
| RSI | Contrarian at RSI extremes |
| Bollinger Breakout | Mean-reversion at band edges |
| Volatility Regime | Switches strategy based on ATR |
| Dual Timeframe | RSI filtered by trend direction |
| Volume Profile | Volume-weighted momentum |

## Docs

- [**Scoring Guide**](docs/scoring-guide.md) — how scoring works, what the leaderboard metrics mean
- [**Strategy Guide**](docs/strategy-guide.md) — practical tips, common approaches, pitfalls
- [**Compare Notebook**](notebooks/compare-strategies.ipynb) — run all strategies side by side

## Model Interface

```python
class TrackerBase:
    def tick(self, data: dict) -> None:
        """Called with each new data batch.
        data = {
            "symbol": "BTCUSDT",
            "asof_ts": 1234567890,

            # Multi-timeframe OHLCV candles
            "candles_1m":  [{"ts": ..., "open": ..., "high": ..., "low": ..., "close": ..., "volume": ...}, ...],
            "candles_5m":  [...],   # last 60 bars (5h)
            "candles_15m": [...],   # last 40 bars (10h)
            "candles_1h":  [...],   # last 24 bars (1 day)

            # Order book snapshot (or None if unavailable)
            "orderbook": {
                "best_bid": 50000.0, "best_ask": 50001.0,
                "spread": 1.0, "mid_price": 50000.5,
                "bid_depth": 123.4, "ask_depth": 98.7,
                "imbalance": 0.11,  # (bid-ask)/(bid+ask), range [-1, 1]
                "bids_top": [[50000.0, 1.5], ...],
                "asks_top": [[50001.0, 1.2], ...],
            },

            # Funding rate / basis (or None if unavailable)
            "funding": {
                "funding_rate": 0.0001,   # current 8h funding rate
                "mark_price": 50000.5,
                "index_price": 50000.0,
                "basis": 0.00001,          # (mark-index)/index
                "next_funding_ts": 1234571490,
            },
        }
        """

    def predict(self, symbol: str, horizon_seconds: int, step_seconds: int) -> dict:
        """Return your prediction.
        Returns: {"expected_return": float}
        """
```
