Files
ai/gateway/knowledge/strategies/strategy-development.md

8.8 KiB
Raw Blame History

Strategy Development Guide

Strategies on Dexorder are PandasStrategy subclasses that receive a live stream of OHLCV bars and call self.buy() / self.sell() / self.flatten() to place orders.

See ../api-reference.md for the DataAPI and ChartingAPI used in research scripts. For indicator calculations, see ../pandas-ta-reference.md.


PandasStrategy API

from dexorder.nautilus.pandas_strategy import PandasStrategy, PandasStrategyConfig

class MyStrategy(PandasStrategy):

    def evaluate(self, dfs: dict[str, pd.DataFrame]) -> None:
        """Called after every new bar across all feeds.

        Args:
            dfs: dict mapping feed_key → pd.DataFrame
                 Columns: timestamp (ns), open, high, low, close, volume,
                          buy_vol, sell_vol, open_interest
                 Rows accumulate over time — last row = latest bar.
        """
        df = dfs.get("BTC/USDT.BINANCE:300")
        if df is None or len(df) < 20:
            return  # not enough data yet

        close = df["close"]
        # ... compute signals ...

        if buy_signal:
            self.buy(quantity=0.1)
        elif sell_signal:
            self.sell(quantity=0.1)

Feed key format

"{SYMBOL.EXCHANGE}:{period_seconds}" — e.g. "BTC/USDT.BINANCE:900" for 15-minute bars.

Access all feeds via self.config.feed_keys (tuple of strings).

Order methods

self.buy(quantity: float, feed_key: str = None)
self.sell(quantity: float, feed_key: str = None)
self.flatten(feed_key: str = None)   # close all open positions

If feed_key is omitted, the first feed in feed_keys is used. quantity is in base currency units (e.g. 0.1 BTC).

Available data

Strategies may only use data in the dfs feeds: crypto OHLCV + buy/sell volume split + open interest. The following are not available:

  • TradFi data (equities, forex, bonds, options, macro indicators)
  • Alternative data (news, social sentiment, on-chain metrics, economic calendars)

Using pandas_ta

Use import pandas_ta as ta for all indicator calculations. Never write manual rolling() or ewm() implementations.

import pandas_ta as ta

rsi = ta.rsi(df["close"], length=14)
macd_df = ta.macd(df["close"], fast=12, slow=26, signal=9)
hist = macd_df.iloc[:, 2]   # histogram column

ema = ta.ema(df["close"], length=20)
atr = ta.atr(df["high"], df["low"], df["close"], length=14)

See ../pandas-ta-reference.md for the full indicator catalog and multi-output column extraction patterns.


Using Custom Indicators

Prefer referencing a custom indicator that already exists in the indicator category rather than duplicating the logic inline. Custom indicators appear on the user's chart, making the signal transparent.

import pandas_ta as ta

def evaluate(self, dfs):
    df = dfs.get("BTC/USDT.BINANCE:3600")
    if df is None or len(df) < 20:
        return

    vw_rsi = ta.custom_vw_rsi(df["close"], df["volume"], length=14)
    if vw_rsi is None or vw_rsi.isna().all():
        return

    if vw_rsi.iloc[-1] < 30:
        self.buy(0.01)
    elif vw_rsi.iloc[-1] > 70:
        self.sell(0.01)

Custom indicator names follow the pattern ta.custom_{sanitized_name}. See ../indicators/indicator-development.md for naming rules and how to create custom indicators.


Strategy Metadata

When writing a strategy with PythonWrite(category="strategy", ...), always provide:

Field Required Description
description yes One-sentence summary
details yes Full markdown: algorithm, entry/exit logic, parameters, data feeds, position sizing. Enough detail to reproduce the code from scratch.
PythonWrite(
    category="strategy",
    name="RSI Mean Reversion",
    description="Buy oversold, sell overbought based on RSI(14) on BTC/USDT 5m bars.",
    details="""## RSI Mean Reversion
...""",
    code="""...""",
    metadata={
        "data_feeds": [
            {"symbol": "BTC/USDT.BINANCE", "period_seconds": 300, "description": "BTC/USDT 5m"}
        ],
        "parameters": {
            "rsi_length": {"default": 14, "description": "RSI lookback period"},
            "oversold":   {"default": 30, "description": "Buy threshold"},
            "overbought": {"default": 70, "description": "Sell threshold"},
            "trade_qty":  {"default": 0.01, "description": "Trade quantity in BTC"}
        }
    }
)

Backtest Workflow

  1. Check existing indicators first: PythonList(category="indicator") — reuse signals already on the chart.
  2. Write the strategy: PythonWrite(...) — runs against synthetic data automatically.
  3. Run a backtest targeting 100,000200,000 bars (max 5 years):
    BacktestStrategy(
        strategy_name="RSI Mean Reversion",
        feeds=[{"symbol": "BTC/USDT.BINANCE", "period_seconds": 900}],
        from_time="2023-01-01",
        to_time="2024-12-31",
        initial_capital=10000
    )
    
  4. Interpret results:
    • summary.total_return — total fractional return (0.15 = +15%)
    • summary.sharpe_ratio — annualized Sharpe (>1.0 good, >2.0 excellent)
    • summary.max_drawdown — maximum peak-to-trough loss
    • summary.win_rate — fraction of profitable trades
    • statistics.profit_factor — gross profit / gross loss (>1.5 good)
  5. Iterate with PythonEdit, re-run backtest.
  6. Activate (paper first): ActivateStrategy(..., paper=True)

Bar resolution and backtest window

Choose the resolution appropriate to the strategy's signal frequency, then set the date range to hit 100k200k bars:

Resolution ~100k bars ~200k bars
5m 1 year 2 years
15m 2.9 years 5 years
1h cap at 5 yr (≈44k bars)
4h cap at 5 yr (≈11k bars)

Strategy Patterns

Trend following

Follow sustained price movements using moving average crossovers, breakout of price channels, or trend-direction filters:

ema_fast = ta.ema(df["close"], length=20)
ema_slow = ta.ema(df["close"], length=50)
bullish = ema_fast.iloc[-1] > ema_slow.iloc[-1]
crossover = ema_fast.iloc[-2] <= ema_slow.iloc[-2]

if bullish and crossover:
    self.buy(qty)

Mean reversion

Profit from price returning to an average after extremes:

rsi = ta.rsi(df["close"], length=14)
if rsi.iloc[-1] < 30:
    self.buy(qty)
elif rsi.iloc[-1] > 70:
    self.sell(qty)

Multi-timeframe confluence

Use a higher-timeframe trend filter with a lower-timeframe entry signal:

df_4h = dfs.get("BTC/USDT.BINANCE:14400")
df_15m = dfs.get("BTC/USDT.BINANCE:900")
if df_4h is None or df_15m is None:
    return

ema_4h = ta.ema(df_4h["close"], length=20)
bullish_trend = df_4h["close"].iloc[-1] > ema_4h.iloc[-1]

macd_df = ta.macd(df_15m["close"])
hist = macd_df.iloc[:, 2]

if bullish_trend and hist.iloc[-1] > 0 and hist.iloc[-2] <= 0:
    self.buy(qty, feed_key="BTC/USDT.BINANCE:900")

Important Rules

  • evaluate() must be fast, lightweight, and deterministic — no model inference, file I/O, network calls, or randomness. It runs on every bar during backtests over potentially hundreds of thousands of bars.
  • No LLM calls inside strategies — strategies must be fully reproducible.
  • Guard for insufficient data — always check len(df) >= min_required before computing indicators with a lookback period.
  • Use .get() for feeds — multi-feed strategies may have feeds missing during warm-up.
  • Size conservatively — a typical trade quantity is 0.0010.01 * initial_capital / price.
  • No import from dexorder inside evaluate() — the strategy file is exec'd in a sandbox; PandasStrategy and pandas_ta are pre-loaded.

Performance Metrics Reference

Metric Good Excellent
Sharpe ratio > 1.0 > 2.0
Profit factor > 1.5 > 2.0
Max drawdown < 20% < 10%
Win rate context-dependent

A strategy with a lower win rate can still be profitable if winners are larger than losers (profit factor > 1). Focus on Sharpe and max drawdown as primary quality metrics.

Avoiding overfitting

  • Do not optimize parameters on the same data used for validation
  • Use a held-out out-of-sample period to verify results
  • Prefer fewer parameters — simpler strategies generalize better
  • Walk-forward analysis: re-fit on a rolling window, evaluate on the next

See Also