TradeReady.io
Backtesting

Strategy Examples

Four complete backtesting strategies with code — moving average crossover, RSI, breakout, and momentum rotation

Download .md

These examples show complete strategy implementations. Each uses the same backtest loop structure — only the decision logic changes.

All examples use Python with httpx. The same logic translates to any language that can make HTTP requests.


Setup

import httpx
import statistics

BASE = "http://localhost:8000/api/v1"
HEADERS = {"X-API-Key": "ak_live_..."}

def create_and_start(label, pairs=None):
    """Create and start a backtest session."""
    r = httpx.post(f"{BASE}/backtest/create", headers=HEADERS, json={
        "start_time": "2025-06-01T00:00:00Z",
        "end_time": "2025-07-01T00:00:00Z",
        "starting_balance": 10000,
        "candle_interval": "1m",
        "strategy_label": label,
        "agent_id": "your-agent-uuid",
        "pairs": pairs,
    })
    sid = r.json()["session_id"]
    httpx.post(f"{BASE}/backtest/{sid}/start", headers=HEADERS)
    return sid

def step(sid, steps=1):
    return httpx.post(
        f"{BASE}/backtest/{sid}/step/batch",
        headers=HEADERS,
        json={"steps": steps}
    ).json()

def place_order(sid, symbol, side, order_type, quantity, price=None):
    body = {"symbol": symbol, "side": side, "type": order_type, "quantity": str(quantity)}
    if price:
        body["price"] = str(price)
    return httpx.post(f"{BASE}/backtest/{sid}/trade/order", headers=HEADERS, json=body).json()

Strategy 1 — Simple Moving Average Crossover

The idea: When the fast (10-period) moving average crosses above the slow (50-period) moving average, a new uptrend may be starting — buy. When the fast crosses back below the slow, sell.

sid = create_and_start("sma_crossover_v1", pairs=["BTCUSDT"])

prices = []
holding = False

while True:
    result = step(sid)  # one candle at a time
    price = float(result["prices"]["BTCUSDT"])
    prices.append(price)

    if len(prices) >= 50:
        fast_ma = statistics.mean(prices[-10:])
        slow_ma = statistics.mean(prices[-50:])

        cash = float(result["portfolio"]["available_cash"])
        equity = float(result["portfolio"]["total_equity"])

        # Buy signal: fast crossed above slow
        if fast_ma > slow_ma and not holding and cash > 10:
            qty = (cash * 0.9) / price  # use 90% of available cash
            place_order(sid, "BTCUSDT", "buy", "market", round(qty, 6))
            holding = True

        # Sell signal: fast crossed below slow
        elif fast_ma < slow_ma and holding:
            positions = httpx.get(
                f"{BASE}/backtest/{sid}/account/positions", headers=HEADERS
            ).json().get("positions", [])
            btc_qty = next(
                (float(p["quantity"]) for p in positions if p["symbol"] == "BTCUSDT"), 0
            )
            if btc_qty > 0:
                place_order(sid, "BTCUSDT", "sell", "market", round(btc_qty, 6))
                holding = False

    if result["is_complete"]:
        break

results = httpx.get(f"{BASE}/backtest/{sid}/results", headers=HEADERS).json()
print(f"SMA Crossover — ROI: {results['summary']['roi_pct']}%")
print(f"Sharpe: {results['metrics']['sharpe_ratio']}")
print(f"Trades: {results['summary']['total_trades']}")

Batching tip: Hourly strategies can step 60 candles at a time — this reduces API calls from 43,200 to 720 for a 30-day test.


Strategy 2 — RSI Mean Reversion

The idea: When RSI drops below 30, the asset is oversold and likely to bounce. When RSI rises above 70, it is overbought and likely to pull back. Buy oversold, sell overbought.

def compute_rsi(prices, period=14):
    if len(prices) < period + 1:
        return None
    changes = [prices[i] - prices[i-1] for i in range(-period, 0)]
    gains = [max(0, c) for c in changes]
    losses = [abs(min(0, c)) for c in changes]
    avg_gain = statistics.mean(gains)
    avg_loss = statistics.mean(losses)
    if avg_loss == 0:
        return 100.0
    rs = avg_gain / avg_loss
    return 100 - (100 / (1 + rs))


sid = create_and_start("rsi_mean_reversion_v1", pairs=["BTCUSDT"])

prices = []
holding = False

while True:
    result = step(sid)
    price = float(result["prices"]["BTCUSDT"])
    prices.append(price)

    rsi = compute_rsi(prices)

    if rsi is not None:
        cash = float(result["portfolio"]["available_cash"])

        # Buy: oversold
        if rsi < 30 and not holding and cash > 10:
            qty = (cash * 0.5) / price  # use half the cash — conservative
            place_order(sid, "BTCUSDT", "buy", "market", round(qty, 6))
            # Set stop loss and take profit automatically
            place_order(sid, "BTCUSDT", "sell", "stop_loss", round(qty, 6),
                        price=price * 0.97)   # -3% stop
            place_order(sid, "BTCUSDT", "sell", "take_profit", round(qty, 6),
                        price=price * 1.05)   # +5% target
            holding = True

        # Sell: overbought (if take-profit hasn't already filled)
        elif rsi > 70 and holding:
            positions = httpx.get(
                f"{BASE}/backtest/{sid}/account/positions", headers=HEADERS
            ).json().get("positions", [])
            btc_qty = next(
                (float(p["quantity"]) for p in positions if p["symbol"] == "BTCUSDT"), 0
            )
            if btc_qty > 0:
                place_order(sid, "BTCUSDT", "sell", "market", round(btc_qty, 6))
                holding = False

    if result["is_complete"]:
        break

results = httpx.get(f"{BASE}/backtest/{sid}/results", headers=HEADERS).json()
print(f"RSI Mean Reversion — ROI: {results['summary']['roi_pct']}%")
print(f"Win Rate: {results['metrics']['win_rate']}%")

Strategy 3 — Breakout with Stop-Loss and Take-Profit

The idea: When price breaks above the 24-hour high, momentum is building and the breakout may continue. Enter and protect with automatic exit orders.

sid = create_and_start("breakout_v1", pairs=["BTCUSDT"])

holding = False
entry_price = None

while True:
    # Step 60 candles (one hour) between decisions
    result = step(sid, steps=60)
    price = float(result["prices"]["BTCUSDT"])

    # Get 24-hour ticker for the high
    ticker = httpx.get(
        f"{BASE}/backtest/{sid}/market/ticker/BTCUSDT", headers=HEADERS
    ).json()
    high_24h = float(ticker.get("high", price))

    # Check if our stop/take-profit orders have filled
    if holding:
        positions = httpx.get(
            f"{BASE}/backtest/{sid}/account/positions", headers=HEADERS
        ).json().get("positions", [])
        btc_qty = next(
            (float(p["quantity"]) for p in positions if p["symbol"] == "BTCUSDT"), 0
        )
        if btc_qty == 0:
            holding = False  # stop or take-profit filled

    # Buy on breakout
    if not holding and price > high_24h:
        cash = float(result["portfolio"]["available_cash"])
        if cash > 10:
            qty = (cash * 0.8) / price
            place_order(sid, "BTCUSDT", "buy", "market", round(qty, 6))
            entry_price = price

            # Automatic exits
            place_order(sid, "BTCUSDT", "sell", "stop_loss", round(qty, 6),
                        price=entry_price * 0.98)   # -2% stop
            place_order(sid, "BTCUSDT", "sell", "take_profit", round(qty, 6),
                        price=entry_price * 1.05)   # +5% target
            holding = True

    if result["is_complete"]:
        break

results = httpx.get(f"{BASE}/backtest/{sid}/results", headers=HEADERS).json()
print(f"Breakout — ROI: {results['summary']['roi_pct']}%")
print(f"Max Drawdown: {results['metrics']['max_drawdown_pct']}%")

Stop-loss and take-profit orders check against every candle during a batch step. Even if you skip 60 candles, your -2% stop will trigger on the correct candle within the batch.


Strategy 4 — Multi-Pair Momentum Rotation

The idea: Every hour, rank all pairs by their 24-hour performance. Hold the top 3. Sell anything that falls out of the top 3 and buy whatever enters it. Always stay with the strongest movers.

PAIRS = ["BTCUSDT", "ETHUSDT", "SOLUSDT", "BNBUSDT", "XRPUSDT"]

sid = create_and_start("momentum_rotation_v1", pairs=PAIRS)

while True:
    # Step one hour (60 1-minute candles) at a time
    result = step(sid, steps=60)

    if result["is_complete"]:
        break

    # Rank pairs by 24h performance
    rankings = []
    for sym in PAIRS:
        ticker = httpx.get(
            f"{BASE}/backtest/{sid}/market/ticker/{sym}", headers=HEADERS
        ).json()
        change_pct = float(ticker.get("change_pct", 0))
        rankings.append((sym, change_pct))

    rankings.sort(key=lambda x: x[1], reverse=True)
    top_3 = {sym for sym, _ in rankings[:3]}

    # Get current holdings
    positions = httpx.get(
        f"{BASE}/backtest/{sid}/account/positions", headers=HEADERS
    ).json().get("positions", [])
    held = {p["symbol"] for p in positions}

    # Sell anything not in top 3
    for pos in positions:
        if pos["symbol"] not in top_3:
            place_order(sid, pos["symbol"], "sell", "market",
                        round(float(pos["quantity"]), 6))

    # Calculate how much to allocate per new position
    portfolio = result["portfolio"]
    cash = float(portfolio["available_cash"])
    to_buy = top_3 - held
    if to_buy and cash > 10:
        alloc = (cash * 0.9) / len(to_buy)
        for sym in to_buy:
            price = float(result["prices"][sym])
            qty = alloc / price
            place_order(sid, sym, "buy", "market", round(qty, 6))

results = httpx.get(f"{BASE}/backtest/{sid}/results", headers=HEADERS).json()
print(f"Momentum Rotation — ROI: {results['summary']['roi_pct']}%")
print(f"Trades: {results['summary']['total_trades']}")

Position Sizing Patterns

All examples above use a simple fixed-percentage approach. Here are three common sizing patterns:

# Fixed percentage: use 10% of equity per trade
qty = (available_cash * 0.10) / current_price

# Equal weight: divide evenly across N positions
qty = (total_equity / N) / current_price

# Risk-based (Kelly-lite): size based on stop-loss distance
risk_amount = total_equity * 0.02  # risk 2% of equity per trade
stop_distance = entry_price - stop_price
qty = risk_amount / stop_distance

Comparing All Strategies

After running all four, compare them:

session_ids = ["bt_sma", "bt_rsi", "bt_breakout", "bt_rotation"]

comparison = httpx.get(
    f"{BASE}/backtest/compare",
    headers=HEADERS,
    params={"sessions": ",".join(session_ids)}
).json()

for c in comparison["comparisons"]:
    print(f"{c['strategy_label']:25} | ROI: {c['roi_pct']:6.1f}% | "
          f"Sharpe: {c['sharpe_ratio']:.2f} | DD: {c['max_drawdown_pct']:.1f}%")

print(f"\nBest ROI: {comparison['best_by_roi']}")
print(f"Best Sharpe: {comparison['best_by_sharpe']}")

Next Steps

On this page