TickAtlas
Developer 12 min read · March 28, 2026

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.

CG
By the TickAtlas team

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.

python
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.

python
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.