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.
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.
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
// 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.
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
# 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
- Backtest passed? Build the live bot
- Need better indicators? Read the top 10 indicators guide
- Want to try different strategies? Explore RSI divergence detection
- Ready to scale? See our scaling guide
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- Tutorial 11 min read
24/7 Crypto Monitoring: Building Always-On Analysis Systems
Build a monitoring system that watches crypto markets around the clock, detects significant moves, and sends alerts when conditions match your criteria.
March 28, 2026
- Tutorial 12 min read
How to Build an AI Market Analyst That Runs 24/7
Build a production-ready AI market analyst that monitors forex and crypto markets around the clock, generates daily briefings, and alerts you to opportunities via Telegram.
March 28, 2026
- Tutorial 10 min read
Using ATR for Dynamic Stop-Loss Placement
Learn how to use Average True Range (ATR) to set volatility-adjusted stop losses that adapt to market conditions, with full code examples.
March 28, 2026