Testing Trading Strategies: Unit Tests, Integration Tests, and Paper Trading
A developer's guide to testing trading systems at every level. From unit testing signal logic to integration testing API calls to paper trading in production.
The Testing Pyramid for Trading
Most trading bot tutorials skip testing entirely. That is a mistake. A trading system has more failure modes than a typical web app, and each failure can cost real money.
[Paper Trading] ← Production-like, no real money
/ \
[Integration Tests] ← API calls, data flow, timing
/ \
[Unit Tests] ← Signal logic, calculations, edge cases
Level 1: Unit Tests
Unit tests verify your signal logic in isolation. No API calls, no network, no databases. Pure inputs and outputs.
import pytest
# The function under test
def evaluate_rsi_signal(rsi: float, direction: str = "long") -> str:
if direction == "long":
if rsi < 30: return "BUY"
if rsi > 70: return "AVOID"
elif direction == "short":
if rsi > 70: return "SELL"
if rsi < 30: return "AVOID"
return "NEUTRAL"
# Unit tests
class TestRSISignal:
def test_oversold_gives_buy(self):
assert evaluate_rsi_signal(25.0, "long") == "BUY"
def test_overbought_gives_avoid_for_long(self):
assert evaluate_rsi_signal(75.0, "long") == "AVOID"
def test_neutral_zone(self):
assert evaluate_rsi_signal(50.0, "long") == "NEUTRAL"
def test_exact_boundary_30(self):
# At exactly 30, not oversold
assert evaluate_rsi_signal(30.0, "long") == "NEUTRAL"
def test_short_direction(self):
assert evaluate_rsi_signal(75.0, "short") == "SELL"
assert evaluate_rsi_signal(25.0, "short") == "AVOID"
def test_extreme_values(self):
assert evaluate_rsi_signal(0.0, "long") == "BUY"
assert evaluate_rsi_signal(100.0, "short") == "SELL"
Unit Testing Position Sizing
def calculate_position_size(
balance: float, risk_pct: float,
entry: float, stop_loss: float, pip_value: float = 10.0
) -> float:
risk_amount = balance * (risk_pct / 100)
stop_distance = abs(entry - stop_loss)
pips = stop_distance * 10000 # for non-JPY pairs
if pips == 0:
return 0.0
lots = risk_amount / (pips * pip_value)
return round(lots, 2)
class TestPositionSizing:
def test_standard_calculation(self):
lots = calculate_position_size(10000, 1.0, 1.0850, 1.0800)
assert lots == 0.2 # $100 risk / (50 pips * $10)
def test_zero_stop_distance(self):
lots = calculate_position_size(10000, 1.0, 1.0850, 1.0850)
assert lots == 0.0 # Should not divide by zero
def test_larger_account(self):
lots = calculate_position_size(50000, 1.0, 1.0850, 1.0800)
assert lots == 1.0
def test_tight_stop(self):
lots = calculate_position_size(10000, 1.0, 1.0850, 1.0840)
assert lots == 1.0 # $100 risk / (10 pips * $10)
Level 2: Integration Tests
Integration tests verify that your code works correctly with the real API. Use them to catch response format changes, authentication issues, and network handling.
import requests
import os
API_KEY = os.environ.get("CLAW_API_KEY")
BASE_URL = "https://tickatlas.com/v1"
@pytest.mark.integration
class TestAPIIntegration:
def test_indicator_returns_valid_rsi(self):
resp = requests.get(
f"{BASE_URL}/indicator",
headers={"X-API-Key": API_KEY},
params={"symbol": "EURUSD", "indicator": "RSI_14", "timeframe": "H4"},
)
assert resp.status_code == 200
data = resp.json()
assert data["success"] is True
rsi = data["data"]["values"]["rsi"]
assert 0 <= rsi <= 100
def test_invalid_symbol_returns_error(self):
resp = requests.get(
f"{BASE_URL}/indicator",
headers={"X-API-Key": API_KEY},
params={"symbol": "INVALID", "indicator": "RSI_14", "timeframe": "H4"},
)
data = resp.json()
assert data["success"] is False
def test_missing_api_key_returns_401(self):
resp = requests.get(
f"{BASE_URL}/indicator",
params={"symbol": "EURUSD", "indicator": "RSI_14", "timeframe": "H4"},
)
assert resp.status_code == 401 Level 3: Paper Trading
Paper trading runs your full strategy with real market data but simulated execution. It is the final validation before risking real money.
class PaperTrader:
"""Simulated trading for strategy validation."""
def __init__(self, initial_balance: float = 10000):
self.balance = initial_balance
self.positions = []
self.trade_history = []
def open_position(self, symbol: str, direction: str,
lots: float, entry: float, sl: float, tp: float):
position = {
"symbol": symbol, "direction": direction,
"lots": lots, "entry": entry, "sl": sl, "tp": tp,
"opened_at": datetime.utcnow().isoformat(),
}
self.positions.append(position)
logger.info(f"PAPER: Opened {direction} {lots} {symbol} @ {entry}")
def check_positions(self, symbol: str, current_price: float):
for pos in self.positions[:]:
if pos["symbol"] != symbol:
continue
if pos["direction"] == "long":
if current_price <= pos["sl"]:
self._close(pos, pos["sl"], "stop_loss")
elif current_price >= pos["tp"]:
self._close(pos, pos["tp"], "take_profit")
elif pos["direction"] == "short":
if current_price >= pos["sl"]:
self._close(pos, pos["sl"], "stop_loss")
elif current_price <= pos["tp"]:
self._close(pos, pos["tp"], "take_profit")
def _close(self, position: dict, exit_price: float, reason: str):
pnl = self._calculate_pnl(position, exit_price)
self.balance += pnl
position["exit"] = exit_price
position["pnl"] = pnl
position["reason"] = reason
self.trade_history.append(position)
self.positions.remove(position)
logger.info(f"PAPER: Closed {position['symbol']} P/L: {pnl:.2f}")
def report(self):
wins = [t for t in self.trade_history if t["pnl"] > 0]
losses = [t for t in self.trade_history if t["pnl"] <= 0]
total = len(self.trade_history)
print(f"Total trades: {total}")
print(f"Win rate: {len(wins)/total:.1%}" if total else "No trades")
print(f"Final balance: \${self.balance:.2f}") When to Move to Live
Unit tests pass with 100% coverage on signal logic
Every edge case in your signal evaluation and position sizing must be tested.
Integration tests confirm API contract
Run these weekly to catch any API changes before they break your live system.
Paper trading for minimum 2 weeks
Two weeks gives you enough trades across different market conditions to validate the strategy's behavior.
Paper results match backtest within tolerance
If paper trading shows a 60% win rate but your backtest showed 80%, something is wrong. Investigate before going live.
Related Reading
Try this with live data
Every account gets $2.50 in free PAYG credits. No card required — paste your API key and run the code above against live broker data.
Keep reading
All articles- Developer 9 min read
Rate Limiting Strategies: How to Maximize Your API Quota
Practical techniques for working within API rate limits. Learn caching, request batching, smart polling, and quota management to get the most out of every API call.
March 28, 2026
- Developer 10 min read
Caching Financial Data: Redis Patterns for Trading Applications
Learn smart caching strategies for financial data using Redis. Reduce API costs, improve latency, and maintain data freshness with TTL-based patterns.
March 28, 2026
- Developer 10 min read
Error Handling in Trading Systems: Why It Matters More Than You Think
Trading systems fail differently than web apps. Learn the error handling patterns that prevent small bugs from becoming expensive losses.
March 28, 2026