Files
ai/gateway/knowledge/strategy-development.md
Tim Olson 47471b7700 Expand model tag support: add GLM-5.1, simplify Anthropic IDs, scan tags anywhere in message
- Flink update_bars debouncing
- update_bars subscription idempotency bugfix
- Price decimal correction bugfix of previous commit
- Add GLM-5.1 model tag alongside renamed GLM-5
- Use short Anthropic model IDs (sonnet/haiku/opus) instead of full version strings
- Allow @tags anywhere in message content, not just at start
- Return hasOtherContent flag instead of trimmed rest string
- Only trigger greeting stream when tag has no other content
- Update workspace knowledge base references to platform/workspace and platform/shapes
- Hierarchical knowledge base catalog
- 151 Trading Strategies knowledge base articles
- Shapes knowledge base article
- MutateShapes tool instead of workspace patch
2026-04-28 15:05:15 -04:00

8.9 KiB
Raw Permalink Blame History

description
description
PandasStrategy class API, order placement, backtesting, and paper trading patterns for automated crypto strategy development.

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 for the DataAPI and ChartingAPI used in research scripts. For indicator calculations, see pandas-ta-reference.


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 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 indicator-development 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