TickAtlas
Tutorial 11 min read · March 28, 2026

How to Backtest a Trading Strategy with Historical API Data

Learn how to backtest your trading strategy using historical OHLC and indicator data from the TickAtlas API. Includes a complete Python backtesting framework.

CG
By the TickAtlas team

Why Backtesting Matters

A strategy that sounds good on paper can lose money in practice. Backtesting runs your strategy against historical data to measure how it would have performed. It is the difference between guessing and knowing.

The TickAtlas API provides historical OHLC data and pre-calculated indicators, making backtesting straightforward — no need to compute indicators yourself.

Step 1: Fetch Historical Data

Pull OHLC candles and indicator values for your target symbol and timeframe. The API supports up to 1000 bars per request.

python
import requests
import pandas as pd

API_KEY = "your_api_key_here"
BASE_URL = "https://tickatlas.com/v1"
HEADERS = {"X-API-Key": API_KEY}

def fetch_historical_data(symbol: str, timeframe: str,
                          bars: int = 500) -> pd.DataFrame:
    """Fetch historical OHLC data from the API."""
    resp = requests.get(f"{BASE_URL}/ohlc", params={
        "symbol": symbol,
        "timeframe": timeframe,
        "bars": bars
    }, headers=HEADERS)
    resp.raise_for_status()
    data = resp.json()["data"]["candles"]
    df = pd.DataFrame(data)
    df["timestamp"] = pd.to_datetime(df["timestamp"])
    df.set_index("timestamp", inplace=True)
    return df

# Fetch 500 H1 candles for EURUSD
ohlc = fetch_historical_data("EURUSD", "H1", 500)
print(f"Fetched {len(ohlc)} candles from {ohlc.index[0]} to {ohlc.index[-1]}")

API Response Format

json
// GET /v1/ohlc?symbol=EURUSD&timeframe=H1&bars=3
{
  "success": true,
  "data": {
    "symbol": "EURUSD",
    "timeframe": "H1",
    "candles": [
      {
        "timestamp": "2026-03-28T10:00:00Z",
        "open": 1.0845,
        "high": 1.0862,
        "low": 1.0838,
        "close": 1.0855,
        "volume": 12450
      },
      {
        "timestamp": "2026-03-28T11:00:00Z",
        "open": 1.0855,
        "high": 1.0871,
        "low": 1.0849,
        "close": 1.0867,
        "volume": 9820
      }
    ]
  }
}

Step 2: Build the Backtester

The backtester iterates through each candle, evaluates your strategy, and tracks hypothetical trades with proper accounting for entry, exit, stop loss, and take profit.

python
class Backtester:
    def __init__(self, initial_balance: float = 10000,
                 risk_per_trade: float = 1.0):
        self.balance = initial_balance
        self.initial_balance = initial_balance
        self.risk_pct = risk_per_trade
        self.trades = []
        self.open_trade = None

    def run(self, df: pd.DataFrame, strategy) -> dict:
        """Run backtest over historical data."""
        for i in range(1, len(df)):
            candle = df.iloc[i]
            prev_candle = df.iloc[i - 1]

            # Check if open trade hits SL or TP
            if self.open_trade:
                self._check_exit(candle)

            # Generate signal if no open trade
            if not self.open_trade:
                signal = strategy.evaluate(candle, prev_candle)
                if signal in ("BUY", "SELL"):
                    self._open_trade(signal, candle, strategy)

        # Close any remaining open trade at last price
        if self.open_trade:
            self._close_trade(df.iloc[-1]["close"], "END_OF_DATA")

        return self._calculate_stats()

    def _open_trade(self, direction, candle, strategy):
        entry = candle["close"]
        atr = candle.get("atr", 0.001)
        sl = entry - (2 * atr) if direction == "BUY" else entry + (2 * atr)
        tp = entry + (3 * atr) if direction == "BUY" else entry - (3 * atr)

        self.open_trade = {
            "direction": direction,
            "entry": entry,
            "sl": sl,
            "tp": tp,
            "time": candle.name
        }

    def _check_exit(self, candle):
        trade = self.open_trade
        if trade["direction"] == "BUY":
            if candle["low"] <= trade["sl"]:
                self._close_trade(trade["sl"], "STOP_LOSS")
            elif candle["high"] >= trade["tp"]:
                self._close_trade(trade["tp"], "TAKE_PROFIT")
        else:
            if candle["high"] >= trade["sl"]:
                self._close_trade(trade["sl"], "STOP_LOSS")
            elif candle["low"] <= trade["tp"]:
                self._close_trade(trade["tp"], "TAKE_PROFIT")

    def _close_trade(self, exit_price, reason):
        trade = self.open_trade
        if trade["direction"] == "BUY":
            pnl = exit_price - trade["entry"]
        else:
            pnl = trade["entry"] - exit_price

        risk_amount = self.balance * (self.risk_pct / 100)
        dollar_pnl = (pnl / abs(trade["entry"] - trade["sl"])) * risk_amount
        self.balance += dollar_pnl

        self.trades.append({
            "direction": trade["direction"],
            "entry": trade["entry"],
            "exit": exit_price,
            "pnl": dollar_pnl,
            "reason": reason
        })
        self.open_trade = None

    def _calculate_stats(self) -> dict:
        if not self.trades:
            return {"total_trades": 0}

        wins = [t for t in self.trades if t["pnl"] > 0]
        losses = [t for t in self.trades if t["pnl"] <= 0]

        return {
            "total_trades": len(self.trades),
            "wins": len(wins),
            "losses": len(losses),
            "win_rate": len(wins) / len(self.trades) * 100,
            "total_pnl": sum(t["pnl"] for t in self.trades),
            "final_balance": self.balance,
            "return_pct": (self.balance - self.initial_balance)
                          / self.initial_balance * 100,
            "max_drawdown": self._max_drawdown()
        }

    def _max_drawdown(self) -> float:
        peak = self.initial_balance
        max_dd = 0
        balance = self.initial_balance
        for trade in self.trades:
            balance += trade["pnl"]
            peak = max(peak, balance)
            dd = (peak - balance) / peak * 100
            max_dd = max(max_dd, dd)
        return round(max_dd, 2)

Step 3: Define a Strategy

class RSIMACDStrategy:
    """Buy when RSI exits oversold and MACD histogram turns positive."""
    def evaluate(self, candle, prev_candle) -> str:
        rsi = candle.get("rsi", 50)
        macd_hist = candle.get("macd_histogram", 0)
        prev_macd_hist = prev_candle.get("macd_histogram", 0)

        # Buy: RSI recovering from oversold + MACD crossing zero
        if rsi < 40 and rsi > 30 and macd_hist > 0 and prev_macd_hist <= 0:
            return "BUY"

        # Sell: RSI falling from overbought + MACD crossing zero
        if rsi > 60 and rsi < 70 and macd_hist < 0 and prev_macd_hist >= 0:
            return "SELL"

        return "HOLD"

Step 4: Run and Analyze

python
# Run the backtest
bt = Backtester(initial_balance=10000, risk_per_trade=1.0)
stats = bt.run(ohlc, RSIMACDStrategy())

print(f"Total trades:   {stats['total_trades']}")
print(f"Win rate:       {stats['win_rate']:.1f}%")
print(f"Total P&L:      \${stats['total_pnl']:.2f}")
print(f"Final balance:  \${stats['final_balance']:.2f}")
print(f"Return:         {stats['return_pct']:.1f}%")
print(f"Max drawdown:   {stats['max_drawdown']:.1f}%")

Key Metrics to Watch

Win Rate

Percentage of trades that are profitable. Above 50% is acceptable if your reward-to-risk ratio is good.

Max Drawdown

The worst peak-to-trough decline. Keep this under 20%. Higher drawdowns are psychologically devastating in live trading.

Profit Factor

Total gross profit divided by total gross loss. Above 1.5 is good. Below 1.0 means the strategy loses money.

Sharpe Ratio

Risk-adjusted return. Above 1.0 is acceptable, above 2.0 is excellent. Below 0.5 means you are not being compensated for the risk.

Common Backtesting Mistakes

Look-Ahead Bias

Using future data in your strategy logic. Always use only the current and past candles when evaluating signals.

Ignoring Spread and Slippage

Add realistic spread costs. Use the /v1/spread endpoint for real spread data on each pair.

Overfitting

If your strategy only works on one specific date range, it is curve-fitted. Test on multiple time periods and instruments.

Survivorship Bias

Only testing on pairs that exist today. Some instruments get delisted. This is less of an issue with major forex pairs.

Next Steps

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.