Algo Lab

Alpha

Algo Lab lets you write Python trading strategies in your browser, test them on real historical data, and run them live on WazirX Futures - all without any setup or external tools.

Backtest

Run your strategy on historical data and get a full report - equity curve, trades, Sharpe ratio, CAGR, and more.

Paper

Simulate trades on real live prices with no real money. Great for testing before going live.

Live

Stream live WazirX prices and place real orders through your webhook or WazirX API keys.

Algo Lab AI Context (Relay / WazirX)

Drop this file into Cursor, Claude Code, ChatGPT, or any AI assistant. It gives your AI everything it needs to write Relay Algo Lab strategies for WazirX Futures — hooks, broker methods, indicators, config keys, guardrails, and examples.

Download

How it works

1

Write a Python class that extends Strategy. Your code auto-saves as a draft while you type.

2

Click Backtest, Paper, or Live in the top bar to open the Run panel.

3

Pick your symbol, capital, leverage, date range, and any risk limits.

4

Your strategy runs and logs appear live in the editor. In paper and live mode, your indicators are pre-warmed with recent historical data before trading begins.

5

When the run finishes, a Results panel opens with your performance report.

6

Every run saves a snapshot of your code. You can restore any old version in one click.

Run Modes

Every strategy supports three modes and two data modes. Choose them in the Run panel.

Modes

ModeDataOrdersUse for
Backtest

Historical candles or trades

Simulated

Measure past performance

Paper

Live prices (real market feed)

Simulated

Test with real prices, no real money

Live - Signal

Live prices

Real orders via webhook

Send orders to multiple accounts via your webhook

Live - Direct

Live prices

Real orders via your API keys

Trade your own WazirX account directly

OHLC mode vs Tick mode

Works in every mode above. Set it in the Run panel.

Data modeHook calledResolutionBest for
OHLC mode
on_ohlc(candle)

1m, 5m, 15m, 1h, 4h, 1d ...

Indicator strategies. Same code works in backtest, paper, and live.

Tick mode
on_tick(data)

Every market trade

Scalping, HFT, and any strategy that needs real-time position or order awareness

When to use tick mode

Tick mode gives you higher accuracy because your strategy reacts to every single market trade. It is the right choice for scalping, HFT-style logic, and spread-based strategies.

Tick mode is required if your strategy needs to check real account state - for example, whether a limit order was filled, what your current position size is, or how much capital is actually free. This is only possible in Direct mode, where your strategy has API key access to a single WazirX account.

In Signal mode, your strategy sends signals to a webhook that fans out to many accounts. It has no API link to any individual account, so calls like get_position() and get_equity() cannot return real account data - regardless of whether you use OHLC or tick mode. If you need real per-account position and order information, you must use Direct mode.

Quick Start

Examples for Relay Algo Lab on WazirX Futures. Strategies always start with from algolab import Strategy

OHLC mode - EMA crossover

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.side = None  # "long" | "short" | None

    def on_ohlc(self, candle):
        close = candle["close"]
        self.fast.update(close)
        self.slow.update(close)

        if not self.slow.is_ready:
            return

        cash = self.broker.available_cash()
        qty  = round((cash * 0.95) / close, 6)

        if self.fast.value > self.slow.value and self.side != "long":
            if self.side == "short":
                self.broker.close_symbol(self.symbol)
            self.broker.buy(self.symbol, qty)
            self.side = "long"
            self.log(f"LONG {qty} @ {close:.2f}")

        elif self.fast.value < self.slow.value and self.side != "short":
            if self.side == "long":
                self.broker.close_symbol(self.symbol)
            self.broker.sell(self.symbol, qty)
            self.side = "short"
            self.log(f"SHORT {qty} @ {close:.2f}")

    def on_stop(self):
        self.broker.close_all()

Tick mode - mean reversion

from collections import deque
from algolab import Strategy

class TickFade(Strategy):
    def on_init(self):
        self.prices = deque(maxlen=100)
        self.side   = None

    def on_tick(self, data):
        # data shape: {"price": float, "qty": float, ...}
        price = data["price"]
        self.prices.append(price)
        if len(self.prices) < 20:
            return

        mean = sum(self.prices) / len(self.prices)
        dev  = (price - mean) / mean

        cash = self.broker.available_cash()
        qty  = round((cash * 0.95) / price, 6)

        if self.side is None:
            if dev < -0.003:               # price 0.3% below mean
                self.broker.buy(self.symbol, qty)
                self.side = "long"
            elif dev > 0.003:
                self.broker.sell(self.symbol, qty)
                self.side = "short"
        else:
            # exit when price returns to mean
            if (self.side == "long"  and dev >= 0) or                (self.side == "short" and dev <= 0):
                self.broker.close_symbol(self.symbol)
                self.side = None

Symbol and capital

self.symbol is the symbol you picked in the Run panel. Use self.broker.available_cash() to size orders - it reflects how much capital is actually free after any open positions.

Strategy API

Lifecycle hooks

MethodWhen it runs
on_init(self)

Once at startup. Create indicators and read config here.

on_start(self)

After on_init, just before the first OHLC candle or tick. Optional.

on_ohlc(self, candle)

Each completed candle (OHLC mode). See candle dict below.

on_tick(self, data)

Each market trade tick (tick mode). See data shape below.

on_order_filled(self, order, trade_record=None)

Backtest only - runs after each simulated fill.

on_stop(self)

When the run ends. Close positions and log final state here.

get_backtest_result(self) -> dict | None

Return a custom result dict to replace the default metrics.

get_status(self) -> dict

Return current strategy state. Used for live health logging.

Strategy properties

PropertyTypeWhat it is
self.symbol

str

The symbol you selected, e.g. BTCINR

self.config

dict

All run settings. Read with self.config.get('key', default)

self.broker

Broker

Place and manage orders - see Broker Methods

self.indicators

IndicatorsFactory

Create indicators - see Indicators

self.warmup_bars

int

Override in on_init() if you use custom (non-factory) indicators. The engine replays this many 1-min historical bars before going live. Default 0 (auto-detected from factory indicators).

self.log(msg)

method

Print a message to the live log panel

self.log_warn(msg)

method

Print a warning

self.log_error(msg)

method

Print an error

self.quit(reason)

method

Stop the run immediately with a reason

The candle dict - on_ohlc()

KeyTypeValue
symbol

str

Trading pair

timestamp

datetime

Candle open time (timezone-aware)

open

float

Open price

high

float

High price

low

float

Low price

close

float

Close price

volume

float

Volume in base asset

The data dict - on_tick()

The shape differs between tick backtest and live/paper. Use the portable helper below:

# Tick backtest (data_mode="tick")
data = {
    "price": 68432.10,       # trade price
    "qty":   0.0015,         # trade quantity  ← note: "qty", not "quantity"
    "time":  1704067200000   # unix timestamp in ms
}

# Paper and live tick mode (bar_interval_sec = 0)
data = {
    "type": "trade",
    "data": {
        "price":    68432.10,
        "quantity": 0.0015,  # ← note: "quantity", not "qty"
    }
}

# Recommended: _parse_tick() handles both shapes
@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:
def on_tick(self, data):
    price, vol = self._parse_tick(data)

Broker Methods

All available via self.broker. In backtest and paper, these simulate fills. In live mode, they place real orders via your webhook or API keys.

Placing orders

MethodWhat it does
buy(symbol, quantity)

Buy at market price (long).

buy(symbol, quantity, price=...)

Place a limit buy.

sell(symbol, quantity)

Sell at market price (short).

sell(symbol, quantity, price=...)

Place a limit sell.

close_symbol(symbol)

Close your entire position for a symbol.

close_all()

Close all open positions.

partial_exit(symbol, percent)

Close part of a position. E.g. 50 closes half.

cancel_order(order_id)

Cancel a specific pending order.

cancel_all(symbol=None)

Cancel all pending orders. Pass symbol to filter.

Checking your position and balance

MethodReturnsWhat it is
available_cash()

float

Capital free for new orders. In backtest and paper this is simulated. In Direct mode it reflects your real account. In Signal mode it returns only the configured capital number.

get_equity()

float | None

Total portfolio value. Simulated in backtest/paper. Real in Direct mode. Not available in Signal mode.

get_position(symbol)

Position | None

Your current open position. Simulated in backtest/paper. Real in Direct mode. Always returns None in Signal mode.

Signal mode has no account access

In Signal mode, your strategy sends signals to a webhook which fans out to multiple accounts. The strategy itself is never linked to any account directly, so it cannot read real positions, real balances, or real open orders. If your strategy logic depends on that information, use Direct mode instead - where your API keys give the strategy access to a single account.

# Size an order using available cash (best practice)
cash = self.broker.available_cash()
qty  = round((cash * 0.95) / candle["close"], 6)
self.broker.buy(self.symbol, qty)

# Only enter if you are not already in a position
pos = self.broker.get_position(self.symbol)
if pos is None:
    self.broker.buy(self.symbol, qty)

# Stop the run early if something goes wrong
if catastrophic_condition:
    self.broker.close_all()
    self.quit("Stopping - unexpected market condition")

How orders work in live mode

In Signal mode, buy() and sell() send a signal to the webhook you picked in the Run panel. That webhook then places orders on all your linked WazirX accounts at the same time. In Direct mode, orders go straight to your account using the API keys you enter in the panel.

Indicators

Create indicators in on_init() using self.indicators.*. Each one has .update(value) to feed new data,.value for the current result, and .is_ready which turns True once it has enough data. Always check .is_ready before using .value. HLC-based indicators (ATR, ADX, etc.) use.update_bar(high, low, close) instead.

Indicator warm-up

In paper and live mode, indicators created via self.indicators.* are pre-warmed with recent historical data before trading begins — so they are ready from the first live candle. No code changes needed.

If you write your own indicator class, set self.warmup_bars = N in on_init() so the platform knows how many bars to replay before going live. In backtest, always check .is_ready before using an indicator's value.

Moving Averages

CreateAttributesDescription
self.indicators.ema(period).value

Exponential Moving Average

self.indicators.sma(period).value

Simple Moving Average

self.indicators.wma(period).value

Weighted Moving Average

self.indicators.vwma(period).value

Volume-Weighted MA - use .update(close, volume)

Oscillators

CreateAttributesDescription
self.indicators.rsi(period=14).value - 0 to 100

Relative Strength Index

self.indicators.macd(fast=12, slow=26, signal=9).macd_line .signal_line .histogram

MACD

self.indicators.cci(period=20).value - use .update_bar(h, l, c)

Commodity Channel Index

self.indicators.stoch(k=14, d=3, smooth=3).k .d

Stochastic Oscillator

self.indicators.momentum(period=10).value

Price Momentum

self.indicators.roc(period=10).value in %

Rate of Change

self.indicators.williams_r(period=14).value - -100 to 0

Williams %R

Volatility

CreateAttributesDescription
self.indicators.atr(period=14).value - use .update_bar(h, l, c)

Average True Range

self.indicators.bb(period=20, std_dev=2.0).upper .middle .lower

Bollinger Bands

self.indicators.std_dev(period=20).value

Rolling Standard Deviation

self.indicators.supertrend(period=10, multiplier=3.0).value .direction (+1 or -1)

Supertrend

self.indicators.true_range().value - use .update_bar(h, l, c)

True Range

Trend

CreateAttributesDescription
self.indicators.adx(period=14).adx .plus_di .minus_di - use .update_bar(h, l, c)

Average Directional Index

self.indicators.ichimoku(tenkan=9, kijun=26, senkou_b=52).tenkan .kijun .senkou_a .senkou_b .chikou

Ichimoku Cloud

self.indicators.aroon(period=25).up .down

Aroon Oscillator

self.indicators.chop(period=14).value - 0 to 100 (above 61 = choppy market)

Choppiness Index

self.indicators.vortex(period=14).plus_vi .minus_vi - use .update_bar(h, l, c)

Vortex Indicator

Volume

CreateAttributesDescription
self.indicators.obv().value - use .update(close, volume)

On-Balance Volume

self.indicators.mfi(period=14).value - 0 to 100 - use .update_bar(h, l, c, volume)

Money Flow Index

self.indicators.vwap().value - use .update_bar(h, l, c, volume)

Volume Weighted Average Price

Other

CreateAttributesDescription
self.indicators.max(period).value

Rolling maximum

self.indicators.min(period).value

Rolling minimum

self.indicators.sum(period).value

Rolling sum

self.indicators.bar_aggregator(multiplier)Higher-timeframe bar builder

Combine lower-timeframe bars into higher-timeframe bars

def on_init(self):
    self.rsi  = self.indicators.rsi(period=14)
    self.bb   = self.indicators.bb(period=20, std_dev=2.0)
    self.atr  = self.indicators.atr(period=14)
    self.macd = self.indicators.macd(fast=12, slow=26, signal=9)

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

    if not self.rsi.is_ready:
        return

    rsi_val    = self.rsi.value            # e.g. 28.4
    bb_lower   = self.bb.lower             # e.g. 67800.0
    atr_val    = self.atr.value            # e.g. 320.5
    macd_cross = self.macd.macd_line > self.macd.signal_line

Config Reference

Everything you set in the Run panel is available in your strategy as self.config. Read any value with self.config.get("key", default). If you hardcode a value in your strategy, it overrides whatever is in the panel.

Backtest

KeyTypeDescription
symbol

str

Trading pair, e.g. BTCINR

data_mode

str

"ohlc" (default) or "tick"

resolution

str

Candle size: 1m 5m 15m 30m 1h 4h 8h 1d (OHLC mode only)

start_date

str

Start date, e.g. 2025-01-01

end_date

str

End date, e.g. 2025-12-31

initial_capital

float

Starting capital in USDT

leverage

int

Leverage multiplier

slippage_pct

float

Simulated fill slippage as a decimal. 0.0001 = 0.01%.

stop_loss_pct

float

Engine-level stop-loss from entry as a decimal. Checked against the bar's intra-bar low (OHLC) or each tick (tick mode). 0 = off.

take_profit_pct

float

Engine-level take-profit from entry as a decimal. Checked against the bar's intra-bar high (OHLC) or each tick (tick mode). 0 = off.

Paper

KeyTypeDescription
symbol

str

Trading pair

initial_capital

float

Starting virtual capital in USDT

leverage

int

Leverage multiplier

bar_interval_sec

int

Candle size in seconds. 0 = tick mode.

Live

KeyTypeDescription
symbol

str

Trading pair

capital

float

Capital for position sizing in USDT

leverage

int

Leverage multiplier

bar_interval_sec

int

Candle size in seconds. 0 = tick mode.

Risk Guardrails

Guardrails stop a run automatically when a risk limit is hit. Set them in the Risk Guardrails section of the Run panel - all are optional but recommended. When a guardrail fires, any open positions are closed and a halt_reason is saved in the result.

Backtest guardrails

Makes your backtest realistic - it stops at the same point a live run would have stopped.

GuardrailWhat it does

Max drawdown %

Stop if your account drops this % from its peak. 0 = off.

Max consecutive losses

Stop after this many losing trades in a row. 0 = off.

Paper guardrails

GuardrailWhat it does

Max drawdown %

Stop if your account drops this % from its peak. 0 = off.

Max consecutive losses

Stop after this many losing trades in a row. 0 = off.

Max duration (days)

Auto-stop after this many days. 0 = no limit.

Feed timeout (min)

Stop if no price tick arrives for this many minutes. Default 5.

Live guardrails

GuardrailWhat it does

Max session drawdown %

Stop if you lose this % from where you started the session. 0 = off.

Max duration (days)

Auto-stop after this many days. 0 = no limit.

Max orders / min

Stop if more than this many orders fire in 60 seconds. 0 = no limit.

Max qty per order

Reject any single order larger than this. 0 = no limit.

Max total orders

Stop after this many orders total in the session. 0 = no limit.

Max consecutive rejects

Stop after this many broker rejections in a row. Default is 5.

Backtest

OHLC mode

Loads historical candles for your symbol, timeframe, and date range, then calls on_ohlc() for each one in order.

Good for most strategies. Even a full year of 1-minute candles runs in seconds.

Tick mode

Replays every individual market trade from historical data, calling on_tick() for each one.

Longer date ranges can take several minutes. A 4-hour limit applies.

What happens step by step

Historical data is loaded for your symbol and date range.

on_ohlc() or on_tick() runs for each data point in order.

buy() and sell() simulate fills at the current price plus slippage. If you set a stop loss or take profit, those trigger automatically.

At the end, on_stop() runs and any remaining open positions are closed at the last price.

If a guardrail limit is hit mid-run, positions close immediately and the run stops with a halt_reason.

A Results panel opens with your metrics, equity chart, trade list, and logs.

Paper Trading

Paper mode uses live WazirX prices and simulates your trades in real time - but never places real orders. Use it to test your strategy on current market conditions before risking real money.

How fills work

Market orders fill at the next tick price after the call.

Your P&L and available cash update after every tick.

Fees are simulated using real WazirX taker rates, including GST.

Orders always fill in full - there are no partial fills.

OHLC mode vs Tick mode

Set bar_interval_sec in the Run panel. Use 0 for tick mode (every market trade calls on_tick()). Any positive number groups ticks into candles and calls on_ohlc().

Auto-detect

The Run panel reads your code and pre-selects the right mode based on whether you defined on_ohlc or on_tick.

Live Mode

Live mode uses the exact same strategy code as backtest and paper. The only difference is that broker calls now place real orders.

Signal vs Direct

Signal mode

Choose a webhook in the panel. When your strategy calls buy() or sell(), the signal goes to that webhook and orders are placed on all linked WazirX accounts at once. Great for managing multiple accounts.

Direct mode

Enter your WazirX API key and secret in the panel. Orders go directly to your account. Your keys are encrypted at rest and fetched securely into your strategy container only at the moment trading begins — they are never stored in logs or any persistent storage.

Order flow in Signal mode

self.broker.buy("BTCINR", quantity=0.001)
    |
    v
Signal sent to your selected webhook
    |
    v
Relay by Trado places orders on all linked accounts
    |
    v
Each account executes a market order on WazirX Futures

Before going live

1.

Backtest and paper trade first - live mode uses real money.

2.

Start with one test account on your webhook, not all of them.

3.

Set guardrails: drawdown limit, max orders/min, and a time limit.

4.

To stop cleanly, hit Cancel in the Active Run panel. This calls on_stop() so your code can close positions.

Result Metrics

When a backtest or paper run finishes, a Results panel opens with three tabs: Performance (metrics and equity chart), Trades (every completed trade), and Logs. You can reopen any past result from the run history at any time.

Performance metrics

FieldTypeWhat it means
initial_capital

float

Starting capital (USDT)

final_capital

float

Ending capital (USDT)

total_return

float

Total profit or loss (USDT)

total_return_pct

float

Return as a % of starting capital

cagr

float

Annualised growth rate (%)

max_drawdown

float

Biggest drop from peak to trough (USDT)

max_drawdown_pct

float

Biggest drop as a % of peak capital

sharpe_ratio

float

Return adjusted for risk. Above 1 is good.

sortino_ratio

float

Like Sharpe but only counts downside risk. Above 1 is good.

profit_factor

float

Total wins divided by total losses. Above 1 = profitable.

expectancy

float

Average profit per trade (USDT)

recovery_factor

float | null

Total return divided by max drawdown

total_trades

int

Number of completed round-trips (entry + exit)

total_orders

int

Total individual order fills

winning_trades

int

Number of profitable trades

losing_trades

int

Number of losing trades

win_rate

float

% of trades that were profitable

avg_win

float

Average profit on a winning trade (USDT)

avg_loss

float

Average loss on a losing trade (USDT, negative)

best_trade_pnl

float

Biggest single trade profit (USDT)

worst_trade_pnl

float

Biggest single trade loss (USDT)

max_consecutive_wins

int

Longest winning streak

max_consecutive_losses

int

Longest losing streak

total_fees

float

Total fees paid (USDT)

halt_reason

str | null

Why the run stopped early, if a guardrail fired.

Equity curve

A list of portfolio snapshots over time - up to 500 points for OHLC backtests, sampled every minute for tick backtests. Each point:

{ "t":      "2025-04-05T00:00:00+00:00",  # timestamp
  "equity": 10842.35,                    # portfolio value
  "price":  68432.10 }                   # asset price

Trade history

Up to 5000 closed round-trips, oldest first. Each entry:

{ "t":        "2025-04-06T14:30:00+00:00",
  "pair":     "BTCINR",
  "side":     "buy",        # direction of the opening leg
  "qty":      0.001,
  "price":    68450.25,     # closing fill price
  "fee":      0.06845,      # fee on this fill
  "pnl":      12.34,        # profit or loss for the round-trip
  "order_id": "abc123" }

Custom Packages

Include a requirements.txt alongside your strategy.py to install additional packages into your strategy container. Only packages on the approved list are accepted — this keeps your strategy environment secure and predictable.

# requirements.txt — include alongside strategy.py
numpy>=1.26.0
scipy>=1.13.0
ta>=0.11.0

Approved packages

These packages are available on request via requirements.txt:

CategoryPackages

Numerics / data

numpy, pandas, scipy, statsmodels

Technical analysis

ta, ta-lib, pandas-ta, tulipy, finta

Machine learning

scikit-learn, joblib

Utilities

python-dateutil, pytz, tqdm, requests

Need a package that isn't listed? Contact support. Packages not on the list are rejected at upload time with a clear error message.

Versions and Drafts

Drafts

The editor saves a draft 2.5 seconds after you stop typing. Drafts survive page reloads and are saved per strategy. A draft is different from a version - a version is only created when you actually run the strategy.

Versions

Every run saves a snapshot of your code as a new version. Running the same code twice does not create a duplicate.

Versions appear in the right panel, newest first.

Click the restore icon to load any old version back into the editor. A banner will confirm it.

Running restored code creates a new version - nothing is overwritten.

Add an optional Version Note in the Run panel before running to label the snapshot.

Versions are stored permanently with no expiry.

Relay by Trado - Algo Lab · relay.trado.trade