Strategy Examples
Four complete backtesting strategies with code — moving average crossover, RSI, breakout, and momentum rotation
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
- Strategy Testing — automated multi-episode testing across random historical periods
- Strategies: Indicators — use platform-computed indicators in your strategy definition
- Gymnasium Environments — train an RL agent that goes beyond rule-based logic