# Algo Lab — AI Context File (Relay by Trado / WazirX Futures)
# Paste into Cursor Rules, Claude Code, ChatGPT system prompt, or any AI assistant.
# Gives your AI full awareness of the Algo Lab strategy API so it can write,
# review, and suggest strategies correctly.
# Last updated: July 2026
#
# Use `from algolab import Strategy` in UI examples (Relay Algo Lab branding).

## PLATFORM
Algo Lab on Relay by Trado (https://relay.trado.trade).
Write Python strategies that run against WazirX Futures data.

## IMPORT
```python
from algolab import Strategy
```
No other platform imports are needed. Standard library and approved pip packages are
available via requirements.txt (see REQUIREMENTS section).

---

## STRATEGY SKELETON

```python
from algolab import Strategy

class MyStrategy(Strategy):
    def on_init(self):
        # Create indicators, read config. Called once at startup.
        pass

    def on_ohlc(self, candle):
        # Called each completed candle (OHLC mode).
        pass

    # OR for tick mode (bar_interval_sec = 0 in paper/live; data_mode="tick" in backtest):
    def on_tick(self, data):
        pass

    def on_start(self):  pass  # optional; after on_init, before first OHLC candle or tick
    def on_stop(self):   pass  # called on shutdown — close positions here
```

---

## LIFECYCLE HOOKS

| Method | When called |
|--------|-------------|
| `on_init(self)` | Once at startup — create indicators, read config |
| `on_start(self)` | After on_init, before first OHLC candle or tick |
| `on_ohlc(self, candle)` | Each completed candle (OHLC mode) |
| `on_tick(self, data)` | Each raw market trade tick (tick mode) |
| `on_order_filled(self, order, trade_record=None)` | Simulated fill callback (backtest only) |
| `on_stop(self)` | Shutdown — close positions, log final state |
| `get_backtest_result(self) -> dict / None` | Return custom metrics dict to override defaults |
| `get_status(self) -> dict` | Live health/status logging |

---

## STRATEGY PROPERTIES

| Property | Description |
|----------|-------------|
| `self.symbol` | Primary symbol from config, e.g. `BTCINR` |
| `self.config` | Full run config dict — read with `self.config.get("key", default)` |
| `self.broker` | Order interface (see Broker Methods) |
| `self.indicators` | Indicator factory (see Indicators) |
| `self.warmup_bars` | Set in `on_init()` if you use custom indicators not created via `self.indicators.*`. The engine replays this many 1-min historical candles before going live. Default 0 (auto-detected from factory indicators). |
| `self.log(msg)` | Log a message to the IDE log panel |
| `self.log_warn(msg)` | Log a warning |
| `self.log_error(msg)` | Log an error |
| `self.quit(reason)` | Stop the session immediately, close positions, record reason |

---

## INDICATOR WARMUP

**Built-in indicators** created via `self.indicators.*` are pre-warmed before the live or
paper feed starts. Your indicators have real data from the first live candle — no cold-start.

**Custom indicator classes** (anything not created via the factory): declare `self.warmup_bars = N`
in `on_init()` to tell the platform how many bars to replay before going live:

```python
def on_init(self):
    self.my_vwap = MyRollingVWAP(window=200)
    self.warmup_bars = 300   # replay 300 x 1-min bars before trading begins
```

**In backtest**, always check `.is_ready` before reading an indicator's value:

```python
def on_ohlc(self, candle):
    self.ema.update(candle["close"])
    if not self.ema.is_ready:
        return   # skip until EMA has enough data
    # trade here
```

---

## CANDLE DICT — on_ohlc()

```python
candle = {
    "symbol":    "BTCINR",
    "timestamp": datetime,   # timezone-aware UTC
    "open":      float,
    "high":      float,
    "low":       float,
    "close":     float,
    "volume":    float,      # base asset volume
}
```

## TRADE DICT — on_tick()

Shape differs by context. Use `_parse_tick()` to handle both:

```python
# Tick backtest (data_mode="tick")
data = {"price": float, "qty": float, "time": int}   # time = unix ms

# Paper and Live tick mode (bar_interval_sec = 0)
data = {"type": "trade", "data": {"price": float, "quantity": float}}

# Recommended: portable helper that works in all tick contexts
@staticmethod
def _parse_tick(data: dict) -> tuple:
    if "data" in data:
        td = data["data"]
        return float(td.get("price", 0)), float(td.get("quantity", 0)) or 1.0
    return float(data.get("price", 0)), float(data.get("qty", 0)) or 1.0

# Usage in on_tick:
def on_tick(self, data: dict):
    price, vol = self._parse_tick(data)
```

---

## BROKER METHODS

All available via `self.broker`.

### Orders
```python
self.broker.buy(symbol, quantity)              # market long
self.broker.buy(symbol, quantity, price=...)   # limit long
self.broker.sell(symbol, quantity)             # market short
self.broker.sell(symbol, quantity, price=...)  # limit short
self.broker.close_symbol(symbol)               # close entire position for symbol
self.broker.close_all()                        # close all open positions
self.broker.partial_exit(symbol, percent)      # reduce position by percent (1–100)
self.broker.cancel_order(order_id)             # cancel a specific pending order
self.broker.cancel_all(symbol=None)            # cancel all orders (optional: by symbol)
```

### Queries
```python
cash = self.broker.available_cash()    # capital free for new orders
eq   = self.broker.get_equity()        # total portfolio value (float | None)
pos  = self.broker.get_position(sym)   # Position object or None if flat
```

**What these return depends on run mode:**
- Backtest / Paper: simulated values, always accurate within the simulation
- Live Direct mode: real values from the account via API keys
- Live Signal mode: `available_cash()` returns only the configured capital number; `get_equity()` and `get_position()` return None — the strategy sends signals to a webhook and is not linked to any single account directly

If your logic depends on real positions, real balances, or real open orders, use **Direct mode**.

### Sizing pattern (best practice)
```python
# Leave 0.2% headroom for taker fees
cash = self.broker.available_cash()
qty  = round((cash * 0.998) / candle["close"], 6)
self.broker.buy(self.symbol, qty)
```

---

## INDICATORS

Create all indicators in `on_init()` using `self.indicators.*`. Every indicator has:
- `.update(value)` — feed new close price
- `.update_bar(high, low, close)` — for HLC-based indicators (ATR, CCI, ADX, etc.)
- `.value` — current output (returns `None` until warm-up complete)
- `.is_ready` — `True` once the indicator has enough data; **always check before using `.value`**

### Moving Averages
```python
self.ema  = self.indicators.ema(period=20)       # .value
self.sma  = self.indicators.sma(period=20)       # .value
self.wma  = self.indicators.wma(period=20)       # .value
self.vwma = self.indicators.vwma(period=20)      # .update_with_volume(close, volume)  .value
```

### Oscillators
```python
self.rsi   = self.indicators.rsi(period=14)                    # .value (0–100)
self.macd  = self.indicators.macd(fast=12, slow=26, signal=9)  # .macd_line  .signal_line  .histogram
self.cci   = self.indicators.cci(period=20)                    # .update_bar(h,l,c)  .value
self.stoch = self.indicators.stoch(k_period=14, d_period=3, smooth=3)  # .update_bar(h,l,c)  .k  .d
self.mom   = self.indicators.momentum(period=10)               # .value
self.roc   = self.indicators.roc(period=10)                    # .value (%)
self.wr    = self.indicators.williams_r(period=14)             # .value (−100 to 0)
```

### Volatility
```python
self.atr = self.indicators.atr(period=14)                        # .update_bar(h,l,c)  .value
self.bb  = self.indicators.bb(period=20, std_dev=2.0)            # .upper  .middle  .lower
self.std = self.indicators.std_dev(period=20)                    # .value
self.st  = self.indicators.supertrend(period=10, multiplier=3.0) # .update_bar(h,l,c)  .value  .trend (+1 or −1; .direction is an alias)
self.tr  = self.indicators.true_range()                          # .update_bar(h,l,c)  .value
```

### Trend
```python
self.adx    = self.indicators.adx(period=14)               # .update_bar(h,l,c)  .value  .plus_di  .minus_di
self.ich    = self.indicators.ichimoku(tenkan=9, kijun=26)  # .tenkan_sen .kijun_sen .senkou_a .senkou_b .chikou_span
self.aroon  = self.indicators.aroon(period=25)             # .aroon_up  .aroon_down  (.up/.down aliases)
self.chop   = self.indicators.chop(period=14)              # .value (>61 = choppy market)
self.vortex = self.indicators.vortex(period=14)            # .update_bar(h,l,c)  .vi_plus  .vi_minus
```

### Volume
```python
self.obv  = self.indicators.obv()           # .update_with_volume(close, volume)  .value
self.mfi  = self.indicators.mfi(period=14)  # .update_with_volume(h,l,c,volume)  .value (0–100)
self.vwap = self.indicators.vwap()          # .update_with_volume(h,l,c,volume)  .value
```

### Statistical & Utility
```python
self.mx   = self.indicators.max(period)          # .value — rolling max
self.mn   = self.indicators.min(period)          # .value — rolling min
self.sm   = self.indicators.sum(period)          # .value — rolling sum
self.bagg = self.indicators.bar_aggregator(mult) # higher-TF bar builder from lower-TF feed
```

### MACD — critical usage note
```python
self.macd.update(close)
if self.macd.is_ready:
    cross_up = self.macd.macd_line > self.macd.signal_line   # NOT .macd / .signal
    hist     = self.macd.histogram
```

---

## RUN MODES

| Mode | Data | Orders | Hook called |
|------|------|--------|-------------|
| Backtest — OHLC | Historical OHLCV candles | Simulated | `on_ohlc()` |
| Backtest — tick | Historical market trades (trade-by-trade) | Simulated | `on_tick()` |
| Paper — OHLC | Live WebSocket feed, aggregated into bars | Simulated | `on_ohlc()` |
| Paper — tick | Live WebSocket feed, every trade | Simulated | `on_tick()` |
| Live Signal — OHLC/tick | Live WebSocket feed | Real, via webhook fan-out | `on_ohlc()` / `on_tick()` |
| Live Direct — OHLC/tick | Live WebSocket feed | Real, via your API keys | `on_ohlc()` / `on_tick()` |

**Note on tick backtest:** Replays every historical market trade — slower than OHLC mode
(up to 4-hour time limit). Stop-loss and take-profit are checked on every tick for the
most accurate fill simulation.

---

## STOP-LOSS AND TAKE-PROFIT

### Engine-level (set in Run panel)
Configure `stop_loss_pct` and `take_profit_pct` in the Run panel for the engine to manage
them automatically.

**In OHLC backtest:** Checked against the bar's **intra-bar high and low** (not just the close),
giving realistic execution — a long position stopped by a candle's low prints at exactly the
stop price, not at the low extreme.

**In tick backtest:** Checked on every individual trade tick — the most accurate simulation.

**In paper mode:** Checked against the full bar range using `update_bar()` — same realism as OHLC backtest.

### Strategy-level (recommended for full control)
Manage stops yourself in `on_ohlc()` / `on_tick()` for maximum flexibility:

```python
def on_init(self):
    self.atr   = self.indicators.atr(period=14)
    self._stop = None
    self._side = None

def on_ohlc(self, candle):
    h, l, c = candle["high"], candle["low"], candle["close"]
    self.atr.update_bar(h, l, c)
    if not self.atr.is_ready:
        return

    # Check stop on intra-bar low (same logic as the engine)
    if self._side == "long" and self._stop and l < self._stop:
        self.broker.close_symbol(self.symbol)
        self._side = None
        return

    if self._side is None:
        qty = round(self.broker.available_cash() * 0.998 / c, 6)
        self.broker.buy(self.symbol, qty)
        self._stop = c - self.atr.value * 2.0   # 2× ATR stop
        self._side = "long"
```

---

## SYMBOL FORMAT

**All WazirX Futures symbols use plain pair codes** (no `B-` prefix, no hyphen separators):
```
BTCINR    ETHINR    SOLINR    BTCUSDT
```

`self.symbol` is always set to the primary symbol the user selected in the Run panel.

### Multi-symbol strategies (pairs, baskets, spreads)

You can trade any symbol from any broker method — not just `self.symbol`. Pass additional symbols via config:

```python
def on_init(self):
    # Pass extras via the "extra_symbols" config key (JSON array in Run panel config)
    self.hedge = self.config.get("hedge_symbol", "ETHINR")

def on_ohlc(self, candle):
    qty = round(self.broker.available_cash() * 0.5 / candle["close"], 6)
    self.broker.buy(self.symbol, qty)           # primary
    self.broker.sell(self.hedge, qty * 20)      # hedge (ETH/BTC spread)
```

> **Note:** In paper/backtest mode the engine simulates fills for any symbol you trade, using the same mark price logic. In live Signal mode, the webhook fan-out handles whichever symbol the signal carries.

---

## CONFIG KEYS (self.config)

### Backtest
```
symbol             str    — primary trading pair, e.g. BTCINR
data_mode          str    — "ohlc" (default) or "tick"
resolution         str    — "1m" "5m" "15m" "30m" "1h" "4h" "8h" "1d" (OHLC only)
start_date         str    — e.g. "2025-01-01"
end_date           str    — e.g. "2025-12-31"
initial_capital    float  — starting capital in USDT
leverage           int    — leverage multiplier
slippage_pct       float  — simulated fill slippage (0.0001 = 0.01%)
stop_loss_pct      float  — auto stop-loss from entry, decimal. 0 = off.
take_profit_pct    float  — auto take-profit from entry, decimal. 0 = off.
```

### Paper
```
symbol             str
initial_capital    float
leverage           int
bar_interval_sec   int    — 0 = tick mode; any positive integer = OHLC bar size in seconds
```

### Live
```
symbol             str
capital            float  — capital for position sizing in USDT
leverage           int
bar_interval_sec   int    — 0 = tick mode; any positive integer = OHLC bar size in seconds
```

Any additional keys you set in the config are available as `self.config.get("key", default)`.
This is the recommended way to pass strategy parameters (band widths, window sizes, etc.)
without hardcoding them.

---

## OUTBOUND HTTP CALLS

Your strategy can make outbound HTTPS calls using `requests` or `httpx` — both are always
available without adding them to `requirements.txt`.

```python
import requests

def on_init(self):
    # Fetch an ML model or signal from your own server
    resp = requests.get("https://my-signals.example.com/btc/signal", timeout=5)
    self.signal = resp.json()
```

**Rules and constraints:**
- **HTTPS only** — HTTP (port 80) is blocked at the network level
- **External servers only** — private/internal IP ranges are blocked
- **Timeout always** — set a reasonable timeout (`timeout=5` to `timeout=30`)
- **Do not block the event loop** — make calls in `on_init()` or async if needed;
  heavy I/O in `on_tick()` will slow tick processing
- **Do not exfiltrate trading data to third parties** — against Terms of Service

---

## REQUIREMENTS.TXT

**Always available without requirements.txt (base image):**
`requests`, `httpx`, `aiohttp`, `pandas`, `numpy`, `python-dateutil`, `pyarrow`

**Approved extra packages (add via requirements.txt):**

| Category | Packages |
|----------|----------|
| Numerics / data | `numpy`, `pandas`, `scipy`, `statsmodels` |
| Technical analysis | `ta`, `ta-lib`, `pandas-ta`, `tulipy`, `finta` |
| ML (inference only) | `scikit-learn`, `joblib` |
| HTTP | `requests`, `aiohttp`, `httpx` |
| Utilities | `python-dateutil`, `pytz`, `tqdm` |

Any unlisted package is rejected at upload time. Contact support to request additions.

```
# requirements.txt example
scipy>=1.13.0
ta>=0.11.0
scikit-learn>=1.4.0
```

---

## RISK GUARDRAILS

Set in the Run Configuration panel. All optional. When triggered: positions close, run stops,
`halt_reason` is recorded in the result.

**Backtest guardrails:**
- `max_drawdown_pct` — halt if equity drops X% from peak (0 = off)
- `max_consecutive_losses` — halt after N back-to-back losses (0 = off)

**Paper guardrails (+ above):**
- `duration_days` — auto-stop after N days (0 = unlimited)
- `feed_timeout_minutes` — halt if no tick arrives for N minutes (default 5)

**Live guardrails:**
- `max_session_drawdown_pct` — halt if equity from session start drops X%
- `duration_days` — auto-stop after N days
- `max_orders_per_min` — halt if > N signals fire in 60 seconds
- `max_qty_per_order` — reject orders above this size
- `max_total_orders` — halt after N total orders in the session
- `max_consecutive_rejects` — halt after N broker rejects in a row (default 5)

---

## RESULT METRICS

```
initial_capital, final_capital, total_return, total_return_pct,
cagr           (null for backtests < 30 days — annualisation is misleading for short runs),
max_drawdown, max_drawdown_pct, sharpe_ratio, sortino_ratio,
profit_factor, expectancy, recovery_factor,
total_trades, total_orders, winning_trades, losing_trades,
win_rate, avg_win, avg_loss, best_trade_pnl, worst_trade_pnl,
max_consecutive_wins, max_consecutive_losses, total_fees,
tax_rate       (float — broker tax rate used in fee calculations, e.g. 0.18 for India),
halt_reason    (str|null — set if a guardrail stopped the run early),
equity_curve  [{"t": ISO, "equity": float, "price": float}],
trades        [{"t": ISO, "pair": str, "side": str, "qty": float,
               "price": float, "fee": float, "pnl": float}]
```

---

## COMPLETE EXAMPLES

### OHLC — EMA crossover with ATR stop
```python
from algolab import Strategy

class EMACross(Strategy):
    def on_init(self):
        self.fast  = self.indicators.ema(period=9)
        self.slow  = self.indicators.ema(period=21)
        self.atr   = self.indicators.atr(period=14)
        self._side = None
        self._stop = None

    def on_ohlc(self, candle):
        h, l, c = candle["high"], candle["low"], candle["close"]
        self.fast.update(c)
        self.slow.update(c)
        self.atr.update_bar(h, l, c)

        if not self.slow.is_ready or not self.atr.is_ready:
            return

        # Check strategy-level ATR stop on intra-bar low
        if self._side == "long" and self._stop and l < self._stop:
            self.broker.close_symbol(self.symbol)
            self._side = None

        qty = round(self.broker.available_cash() * 0.998 / c, 6)

        if self.fast.value > self.slow.value and self._side != "long":
            if self._side == "short":
                self.broker.close_symbol(self.symbol)
            if qty > 0.001:
                self.broker.buy(self.symbol, qty)
                self._stop = c - self.atr.value * 2.0
                self._side = "long"
                self.log(f"LONG {qty:.6f} @ {c:.2f}  stop={self._stop:.2f}")

        elif self.fast.value < self.slow.value and self._side != "short":
            if self._side == "long":
                self.broker.close_symbol(self.symbol)
            if qty > 0.001:
                self.broker.sell(self.symbol, qty)
                self._stop = c + self.atr.value * 2.0
                self._side = "short"
                self.log(f"SHORT {qty:.6f} @ {c:.2f}  stop={self._stop:.2f}")

    def on_stop(self):
        self.broker.close_all()
        self.log(f"Stopped. Final equity: {self.broker.get_equity()}")
```

### Tick — VWAP mean-reversion (works in both live and tick backtest)
```python
from collections import deque
from algolab import Strategy

class TickVWAP(Strategy):
    def on_init(self):
        cfg = self.config
        self.band    = float(cfg.get("vwap_band", 0.003)) / 100
        self.exit_   = float(cfg.get("vwap_exit", 0.001)) / 100
        self.stop    = float(cfg.get("stop_pct",  0.015)) / 100
        n            = int(cfg.get("vwap_window", 100))
        self._pv     = deque(maxlen=n)
        self._vol    = deque(maxlen=n)
        self._side   = None
        self._entry  = 0.0
        self._ticks  = 0

    @staticmethod
    def _parse_tick(data: dict) -> tuple:
        """Handle both live and tick-backtest data shapes."""
        if "data" in data:
            td = data["data"]
            return float(td.get("price", 0)), float(td.get("quantity", 0)) or 1.0
        return float(data.get("price", 0)), float(data.get("qty", 0)) or 1.0

    def on_tick(self, data: dict):
        price, vol = self._parse_tick(data)
        if not price:
            return
        self._ticks += 1
        self._pv.append(price * vol)
        self._vol.append(vol)
        total = sum(self._vol)
        vwap = sum(self._pv) / total if total else price
        if self._ticks < len(self._pv) // 4:
            return
        dev = (price - vwap) / vwap

        if self._side == "long":
            pnl = (price - self._entry) / self._entry
            if dev >= -self.exit_ or pnl <= -self.stop:
                self.broker.close_symbol(self.symbol)
                self._side = None
            return
        if self._side == "short":
            pnl = (self._entry - price) / self._entry
            if dev <= self.exit_ or pnl <= -self.stop:
                self.broker.close_symbol(self.symbol)
                self._side = None
            return

        qty = round(self.broker.available_cash() * 0.998 / price, 6)
        if qty <= 0:
            return
        if dev < -self.band:
            self.broker.buy(self.symbol, qty)
            self._side, self._entry = "long", price
        elif dev > self.band:
            self.broker.sell(self.symbol, qty)
            self._side, self._entry = "short", price

    def on_stop(self):
        self.log(f"Stopped after {self._ticks} ticks")
```

---

## AI ASSISTANT RULES

1. Always import with `from algolab import Strategy`.
2. Create indicators in `on_init()`, never in `on_ohlc()` or `on_tick()`.
3. Always check `.is_ready` before reading `.value`.
4. MACD: canonical accessors are `.macd_line` and `.signal_line` (`.macd` / `.signal` work as aliases).
5. ATR, CCI, ADX, Supertrend, Vortex, TrueRange: use `.update_bar(high, low, close)`.
6. Size orders with `self.broker.available_cash() * 0.998` (leave 0.2% for taker fees).
7. Never use `get_position()` or `get_equity()` in Signal mode strategies — they return None. Use Direct mode if you need real account state.
8. `on_tick()` data shape differs between tick backtest and live/paper — always use `_parse_tick()` or handle both shapes explicitly.
9. Close positions in `on_stop()` for clean shutdown.
10. If you write custom indicator classes (not via `self.indicators.*`), set `self.warmup_bars = N` in `on_init()` so the engine pre-warms them before going live.
11. Symbol format: `BTCINR`, `ETHINR`, `BTCUSDT`, etc. — not `B-BTC_USDT` or `BTC-USDT`. Always use `self.symbol` for the primary symbol; hardcode additional symbols the same way or pass them via `self.config`.
12. `cagr` is `null` (shown as `—`) for backtests shorter than 30 days.
13. Outbound HTTP: use `requests` or `httpx` (always available, no requirements.txt needed). Always set a timeout. HTTPS only. Do not call private/internal IPs.
14. Do NOT hardcode `self.config` keys that are set in the Run panel (symbol, leverage, etc.) — always read them via `self.config.get(...)` so strategies are reusable.
