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.
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.
Every strategy supports three modes and two data modes. Choose them in the Run panel.
| Mode | Data | Orders | Use 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 |
Works in every mode above. Set it in the Run panel.
| Data mode | Hook called | Resolution | Best 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.
Examples for Relay Algo Lab on WazirX Futures. Strategies always start with from algolab import Strategy
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()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 = NoneSymbol 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.
| Method | When 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. |
| Property | Type | What 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 |
candle dict - on_ohlc()| Key | Type | Value |
|---|---|---|
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 |
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)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.
| Method | What 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. |
| Method | Returns | What 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.
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.
| Create | Attributes | Description |
|---|---|---|
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) |
| Create | Attributes | Description |
|---|---|---|
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 |
| Create | Attributes | Description |
|---|---|---|
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 |
| Create | Attributes | Description |
|---|---|---|
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 |
| Create | Attributes | Description |
|---|---|---|
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 |
| Create | Attributes | Description |
|---|---|---|
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_lineEverything 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.
| Key | Type | Description |
|---|---|---|
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. |
| Key | Type | Description |
|---|---|---|
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. |
| Key | Type | Description |
|---|---|---|
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. |
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.
Makes your backtest realistic - it stops at the same point a live run would have stopped.
| Guardrail | What 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. |
| Guardrail | What 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. |
| Guardrail | What 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. |
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.
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.
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 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.
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.
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 uses the exact same strategy code as backtest and paper. The only difference is that broker calls now place real orders.
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.
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.
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 FuturesBefore 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.
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.
| Field | Type | What 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. |
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 priceUp 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" }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
These packages are available on request via requirements.txt:
| Category | Packages |
|---|---|
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.
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.
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